1387 lines
47 KiB
C++
1387 lines
47 KiB
C++
/*
|
|
This file is part of Warzone 2100.
|
|
Copyright (C) 2013 Warzone 2100 Project
|
|
|
|
Warzone 2100 is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Warzone 2100 is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Warzone 2100; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
/**
|
|
* @file qtscript.cpp
|
|
*
|
|
* New scripting system
|
|
*/
|
|
|
|
#include "qtscript.h"
|
|
|
|
#include <QtCore/QTextStream>
|
|
#include <QtCore/QHash>
|
|
#include <QtScript/QScriptEngine>
|
|
#include <QtScript/QScriptValue>
|
|
#include <QtScript/QScriptValueIterator>
|
|
#include <QtScript/QScriptSyntaxCheckResult>
|
|
#include <QtCore/QList>
|
|
#include <QtCore/QString>
|
|
#include <QtCore/QStringList>
|
|
#include <QtCore/QFileInfo>
|
|
#include <QtGui/QStandardItemModel>
|
|
#include <QtGui/QFileDialog>
|
|
|
|
#include "lib/framework/wzapp.h"
|
|
#include "lib/framework/wzconfig.h"
|
|
#include "lib/framework/file.h"
|
|
#include "lib/gamelib/gtime.h"
|
|
#include "multiplay.h"
|
|
#include "map.h"
|
|
#include "difficulty.h"
|
|
#include "console.h"
|
|
#include "lib/netplay/netplay.h"
|
|
|
|
#include "qtscriptdebug.h"
|
|
#include "qtscriptfuncs.h"
|
|
|
|
#define ATTACK_THROTTLE 1000
|
|
|
|
typedef QList<QStandardItem *> QStandardItemList;
|
|
|
|
/// selection changes are too often and too erratic to trigger immediately,
|
|
/// so until we have a queue system for events, delay triggering this way.
|
|
static bool selectionChanged = false;
|
|
extern bool doUpdateModels; // ugh-ly hack; fix with signal when moc-ing this file
|
|
|
|
enum timerType
|
|
{
|
|
TIMER_REPEAT, TIMER_ONESHOT_READY, TIMER_ONESHOT_DONE
|
|
};
|
|
|
|
struct timerNode
|
|
{
|
|
QString function;
|
|
QScriptEngine *engine;
|
|
int baseobj;
|
|
OBJECT_TYPE baseobjtype;
|
|
QString stringarg;
|
|
int frameTime;
|
|
int ms;
|
|
int player;
|
|
int calls;
|
|
timerType type;
|
|
timerNode() {}
|
|
timerNode(QScriptEngine *caller, QString val, int plr, int frame)
|
|
: function(val), engine(caller), baseobj(-1), frameTime(frame + gameTime), ms(frame), player(plr), calls(0), type(TIMER_REPEAT) {}
|
|
bool operator== (const timerNode &t) { return function == t.function && player == t.player; }
|
|
// implement operator less TODO
|
|
};
|
|
|
|
#define MAX_MS 20
|
|
#define HALF_MAX_MS 10
|
|
|
|
/// List of timer events for scripts. Before running them, we sort the list then run as many as we have time for.
|
|
/// In this way, we implement load balancing of events and keep frame rates tidy for users. Since scripts run on the
|
|
/// host, we do not need to worry about each peer simulating the world differently.
|
|
static QList<timerNode> timers;
|
|
|
|
/// Scripting engine (what others call the scripting context, but QtScript's nomenclature is different).
|
|
static QList<QScriptEngine *> scripts;
|
|
|
|
/// Remember what names are used internally in the scripting engine, we don't want to save these to the savegame
|
|
static QHash<QString, int> internalNamespace;
|
|
|
|
typedef struct monitor_bin
|
|
{
|
|
int worst;
|
|
uint32_t worstGameTime;
|
|
int calls;
|
|
int overMaxTimeCalls;
|
|
int overHalfMaxTimeCalls;
|
|
uint64_t time;
|
|
monitor_bin() : worst(0), worstGameTime(0), calls(0), overMaxTimeCalls(0), overHalfMaxTimeCalls(0), time(0) {}
|
|
} MONITOR_BIN;
|
|
typedef QHash<QString, MONITOR_BIN> MONITOR;
|
|
static QHash<QScriptEngine *, MONITOR *> monitors;
|
|
|
|
static MODELMAP models;
|
|
static QStandardItemModel *triggerModel;
|
|
static bool globalDialog = false;
|
|
|
|
static void updateGlobalModels();
|
|
|
|
// ----------------------------------------------------------
|
|
|
|
// Call a function by name
|
|
static bool callFunction(QScriptEngine *engine, const QString &function, const QScriptValueList &args, bool required = false)
|
|
{
|
|
code_part level = required ? LOG_ERROR : LOG_SCRIPT;
|
|
QScriptValue value = engine->globalObject().property(function);
|
|
if (!value.isValid() || !value.isFunction())
|
|
{
|
|
// not necessarily an error, may just be a trigger that is not defined (ie not needed)
|
|
// or it could be a typo in the function name or ...
|
|
debug(level, "called function (%s) not defined", function.toUtf8().constData());
|
|
return false;
|
|
}
|
|
int ticks = wzGetTicks();
|
|
QScriptValue result = value.call(QScriptValue(), args);
|
|
ticks = wzGetTicks() - ticks;
|
|
MONITOR *monitor = monitors.value(engine); // pick right one for this engine
|
|
MONITOR_BIN m;
|
|
if (monitor->contains(function))
|
|
{
|
|
m = monitor->value(function);
|
|
}
|
|
if (ticks > MAX_MS)
|
|
{
|
|
debug(LOG_SCRIPT, "%s took %d ms at time %d", function.toUtf8().constData(), ticks, wzGetTicks());
|
|
m.overMaxTimeCalls++;
|
|
}
|
|
else if (ticks > HALF_MAX_MS)
|
|
{
|
|
m.overHalfMaxTimeCalls++;
|
|
}
|
|
m.calls++;
|
|
if (ticks > m.worst)
|
|
{
|
|
m.worst = ticks;
|
|
m.worstGameTime = gameTime;
|
|
}
|
|
m.time += ticks;
|
|
monitor->insert(function, m);
|
|
if (engine->hasUncaughtException())
|
|
{
|
|
int line = engine->uncaughtExceptionLineNumber();
|
|
QStringList bt = engine->uncaughtExceptionBacktrace();
|
|
for (int i = 0; i < bt.size(); i++)
|
|
{
|
|
debug(LOG_ERROR, "%d : %s", i, bt.at(i).toUtf8().constData());
|
|
}
|
|
ASSERT(false, "Uncaught exception calling function \"%s\" at line %d: %s",
|
|
function.toUtf8().constData(), line, result.toString().toUtf8().constData());
|
|
engine->clearExceptions();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//-- \subsection{setTimer(function, milliseconds[, object])}
|
|
//-- Set a function to run repeated at some given time interval. The function to run
|
|
//-- is the first parameter, and it \underline{must be quoted}, otherwise the function will
|
|
//-- be inlined. The second parameter is the interval, in milliseconds. A third, optional
|
|
//-- parameter can be a \emph{game object} to pass to the timer function. If the \emph{game object}
|
|
//-- dies, the timer stops running. The minimum number of milliseconds is 100, but such
|
|
//-- fast timers are strongly discouraged as they may deteriorate the game performance.
|
|
//--
|
|
//-- \begin{lstlisting}
|
|
//-- function conDroids()
|
|
//-- {
|
|
//-- ... do stuff ...
|
|
//-- }
|
|
//-- // call conDroids every 4 seconds
|
|
//-- setTimer("conDroids", 4000);
|
|
//-- \end{lstlisting}
|
|
static QScriptValue js_setTimer(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
SCRIPT_ASSERT(context, context->argument(0).isString(), "Timer functions must be quoted");
|
|
QString funcName = context->argument(0).toString();
|
|
QScriptValue ms = context->argument(1);
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
timerNode node(engine, funcName, player, ms.toInt32());
|
|
QScriptValue value = engine->globalObject().property(funcName); // check existence
|
|
SCRIPT_ASSERT(context, value.isValid() && value.isFunction(), "No such function: %s",
|
|
funcName.toUtf8().constData());
|
|
if (context->argumentCount() == 3)
|
|
{
|
|
QScriptValue obj = context->argument(2);
|
|
if (obj.isString())
|
|
{
|
|
node.stringarg = obj.toString();
|
|
}
|
|
else // is game object
|
|
{
|
|
node.baseobj = obj.property("id").toInt32();
|
|
node.baseobjtype = (OBJECT_TYPE)obj.property("type").toInt32();
|
|
}
|
|
}
|
|
node.type = TIMER_REPEAT;
|
|
timers.push_back(node);
|
|
return QScriptValue();
|
|
}
|
|
|
|
//-- \subsection{removeTimer(function)}
|
|
//-- Removes an existing timer. The first parameter is the function timer to remove,
|
|
//-- and its name \underline{must be quoted}.
|
|
static QScriptValue js_removeTimer(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
SCRIPT_ASSERT(context, context->argument(0).isString(), "Timer functions must be quoted");
|
|
QString function = context->argument(0).toString();
|
|
int i, size = timers.size();
|
|
for (i = 0; i < size; ++i)
|
|
{
|
|
timerNode node = timers.at(i);
|
|
if (node.function == function)
|
|
{
|
|
timers.removeAt(i);
|
|
break;
|
|
}
|
|
}
|
|
if (i == size)
|
|
{
|
|
// Friendly warning
|
|
QString warnName = function.left(15) + "...";
|
|
debug(LOG_ERROR, "Did not find timer %s to remove", warnName.toUtf8().constData());
|
|
}
|
|
return QScriptValue();
|
|
}
|
|
|
|
//-- \subsection{queue(function[, milliseconds[, object]])}
|
|
//-- Queues up a function to run at a later game frame. This is useful to prevent
|
|
//-- stuttering during the game, which can happen if too much script processing is
|
|
//-- done at once. The function to run is the first parameter, and it
|
|
//-- \underline{must be quoted}, otherwise the function will be inlined.
|
|
//-- The second parameter is the delay in milliseconds, if it is omitted or 0,
|
|
//-- the function will be run at a later frame. A third optional
|
|
//-- parameter can be a \emph{game object} to pass to the queued function. If the \emph{game object}
|
|
//-- dies before the queued call runs, nothing happens.
|
|
// TODO, check if an identical call is already queued up - and in this case,
|
|
// do not add anything.
|
|
static QScriptValue js_queue(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
SCRIPT_ASSERT(context, context->argument(0).isString(), "Queued functions must be quoted");
|
|
QString funcName = context->argument(0).toString();
|
|
QScriptValue value = engine->globalObject().property(funcName); // check existence
|
|
SCRIPT_ASSERT(context, value.isValid() && value.isFunction(), "No such function: %s",
|
|
funcName.toUtf8().constData());
|
|
int ms = 0;
|
|
if (context->argumentCount() > 1)
|
|
{
|
|
ms = context->argument(1).toInt32();
|
|
}
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
timerNode node(engine, funcName, player, ms);
|
|
if (context->argumentCount() == 3)
|
|
{
|
|
QScriptValue obj = context->argument(2);
|
|
if (obj.isString())
|
|
{
|
|
node.stringarg = obj.toString();
|
|
}
|
|
else // is game object
|
|
{
|
|
node.baseobj = obj.property("id").toInt32();
|
|
node.baseobjtype = (OBJECT_TYPE)obj.property("type").toInt32();
|
|
}
|
|
}
|
|
node.type = TIMER_ONESHOT_READY;
|
|
timers.push_back(node);
|
|
return QScriptValue();
|
|
}
|
|
|
|
void scriptRemoveObject(BASE_OBJECT *psObj)
|
|
{
|
|
// Weed out timers with dead objects
|
|
for (int i = 0; i < timers.count(); )
|
|
{
|
|
const timerNode node = timers.at(i);
|
|
if (node.baseobj == psObj->id)
|
|
{
|
|
timers.removeAt(i);
|
|
}
|
|
else
|
|
{
|
|
i++;
|
|
}
|
|
}
|
|
groupRemoveObject(psObj);
|
|
}
|
|
|
|
//-- \subsection{include(file)}
|
|
//-- Includes another source code file at this point. You should generally only specify the filename,
|
|
//-- not try to specify its path, here.
|
|
static QScriptValue js_include(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
QString basePath = engine->globalObject().property("scriptPath").toString();
|
|
QFileInfo basename(context->argument(0).toString());
|
|
QString path = basePath + "/" + basename.fileName();
|
|
// allow users to use subdirectories too
|
|
if (PHYSFS_exists(basename.filePath().toUtf8().constData()))
|
|
{
|
|
path = basename.filePath(); // use this path instead (from read-only dir)
|
|
}
|
|
else if (PHYSFS_exists(QString("scripts/" + basename.filePath()).toUtf8().constData()))
|
|
{
|
|
path = "scripts/" + basename.filePath(); // use this path instead (in user write dir)
|
|
}
|
|
UDWORD size;
|
|
char *bytes = NULL;
|
|
if (!loadFile(path.toUtf8().constData(), &bytes, &size))
|
|
{
|
|
debug(LOG_ERROR, "Failed to read include file \"%s\" (path=%s, name=%s)",
|
|
path.toUtf8().constData(), basePath.toUtf8().constData(), basename.filePath().toUtf8().constData());
|
|
return QScriptValue(false);
|
|
}
|
|
QString source = QString::fromUtf8(bytes, size);
|
|
free(bytes);
|
|
QScriptSyntaxCheckResult syntax = QScriptEngine::checkSyntax(source);
|
|
if (syntax.state() != QScriptSyntaxCheckResult::Valid)
|
|
{
|
|
debug(LOG_ERROR, "Syntax error in include %s line %d: %s",
|
|
path.toUtf8().constData(), syntax.errorLineNumber(), syntax.errorMessage().toUtf8().constData());
|
|
return QScriptValue(false);
|
|
}
|
|
context->setActivationObject(engine->globalObject());
|
|
context->setThisObject(engine->globalObject());
|
|
QScriptValue result = engine->evaluate(source, path);
|
|
if (engine->hasUncaughtException())
|
|
{
|
|
int line = engine->uncaughtExceptionLineNumber();
|
|
debug(LOG_ERROR, "Uncaught exception at line %d, include file %s: %s",
|
|
line, path.toUtf8().constData(), result.toString().toUtf8().constData());
|
|
return QScriptValue(false);
|
|
}
|
|
debug(LOG_SCRIPT, "Included new script file %s", path.toUtf8().constData());
|
|
return QScriptValue(true);
|
|
}
|
|
|
|
// do not want to call this 'init', since scripts are often loaded before we get here
|
|
bool prepareScripts()
|
|
{
|
|
debug(LOG_WZ, "Scripts prepared");
|
|
prepareLabels();
|
|
return true;
|
|
}
|
|
|
|
bool initScripts()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool shutdownScripts()
|
|
{
|
|
jsDebugShutdown();
|
|
globalDialog = false;
|
|
models.clear();
|
|
triggerModel = NULL;
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
MONITOR *monitor = monitors.value(engine);
|
|
QString scriptName = engine->globalObject().property("scriptName").toString();
|
|
int me = engine->globalObject().property("me").toInt32();
|
|
dumpScriptLog(scriptName, me, "=== PERFORMANCE DATA ===\n");
|
|
for (MONITOR::const_iterator iter = monitor->constBegin(); iter != monitor->constEnd(); ++iter)
|
|
{
|
|
QString function = iter.key();
|
|
MONITOR_BIN m = iter.value();
|
|
QString info = function;
|
|
info += " : " + QString::number(m.calls) + " calls; ";
|
|
info += QString::number(m.time / m.calls) + "ms average; ";
|
|
info += QString::number(m.worst) + "ms worst (at " + QString::number(m.worstGameTime) + "); ";
|
|
info += QString::number(m.overMaxTimeCalls) + " calls over limit; ";
|
|
info += QString::number(m.overHalfMaxTimeCalls) + " calls over half limit.\n";
|
|
dumpScriptLog(scriptName, me, info);
|
|
}
|
|
monitor->clear();
|
|
delete monitor;
|
|
unregisterFunctions(engine);
|
|
}
|
|
timers.clear();
|
|
internalNamespace.clear();
|
|
monitors.clear();
|
|
while (!scripts.isEmpty())
|
|
{
|
|
delete scripts.takeFirst();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool updateScripts()
|
|
{
|
|
// Call delayed triggers here
|
|
if (selectionChanged)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += js_enumSelected(NULL, engine);
|
|
callFunction(engine, "eventSelectionChanged", args);
|
|
}
|
|
selectionChanged = false;
|
|
}
|
|
|
|
// Update gameTime
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
|
|
engine->globalObject().setProperty("gameTime", gameTime, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
}
|
|
// Weed out dead timers
|
|
for (int i = 0; i < timers.count(); )
|
|
{
|
|
const timerNode node = timers.at(i);
|
|
if (node.type == TIMER_ONESHOT_DONE)
|
|
{
|
|
timers.removeAt(i);
|
|
}
|
|
else
|
|
{
|
|
i++;
|
|
}
|
|
}
|
|
// Check for timers, and run them if applicable.
|
|
// TODO - load balancing
|
|
QList<timerNode> runlist; // make a new list here, since we might trample all over the timer list during execution
|
|
QList<timerNode>::iterator iter;
|
|
for (iter = timers.begin(); iter != timers.end(); iter++)
|
|
{
|
|
if (iter->frameTime <= gameTime)
|
|
{
|
|
iter->frameTime = iter->ms + gameTime; // update for next invokation
|
|
if (iter->type == TIMER_ONESHOT_READY)
|
|
{
|
|
iter->type = TIMER_ONESHOT_DONE; // unless there is none
|
|
}
|
|
iter->calls++;
|
|
runlist.append(*iter);
|
|
}
|
|
}
|
|
for (iter = runlist.begin(); iter != runlist.end(); iter++)
|
|
{
|
|
QScriptValueList args;
|
|
if (iter->baseobj > 0)
|
|
{
|
|
args += convMax(IdToObject(iter->baseobjtype, iter->baseobj, iter->player), iter->engine);
|
|
}
|
|
else if (!iter->stringarg.isEmpty())
|
|
{
|
|
args += iter->stringarg;
|
|
}
|
|
callFunction(iter->engine, iter->function, args, true);
|
|
}
|
|
|
|
if (globalDialog && doUpdateModels)
|
|
{
|
|
updateGlobalModels();
|
|
doUpdateModels = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QScriptEngine *loadPlayerScript(QString path, int player, int difficulty)
|
|
{
|
|
ASSERT_OR_RETURN(NULL, player < MAX_PLAYERS, "Player index %d out of bounds", player);
|
|
QScriptEngine *engine = new QScriptEngine();
|
|
UDWORD size;
|
|
char *bytes = NULL;
|
|
if (!loadFile(path.toUtf8().constData(), &bytes, &size))
|
|
{
|
|
debug(LOG_ERROR, "Failed to read script file \"%s\"", path.toUtf8().constData());
|
|
return NULL;
|
|
}
|
|
QString source = QString::fromUtf8(bytes, size);
|
|
free(bytes);
|
|
QScriptSyntaxCheckResult syntax = QScriptEngine::checkSyntax(source);
|
|
ASSERT_OR_RETURN(NULL, syntax.state() == QScriptSyntaxCheckResult::Valid, "Syntax error in %s line %d: %s",
|
|
path.toUtf8().constData(), syntax.errorLineNumber(), syntax.errorMessage().toUtf8().constData());
|
|
// Special functions
|
|
engine->globalObject().setProperty("setTimer", engine->newFunction(js_setTimer));
|
|
engine->globalObject().setProperty("queue", engine->newFunction(js_queue));
|
|
engine->globalObject().setProperty("removeTimer", engine->newFunction(js_removeTimer));
|
|
engine->globalObject().setProperty("include", engine->newFunction(js_include));
|
|
|
|
// Special global variables
|
|
//== \item[version] Current version of the game, set in \emph{major.minor} format.
|
|
engine->globalObject().setProperty("version", "3.2", QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[selectedPlayer] The player ontrolled by the client on which the script runs.
|
|
engine->globalObject().setProperty("selectedPlayer", selectedPlayer, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[gameTime] The current game time. Updated before every invokation of a script.
|
|
engine->globalObject().setProperty("gameTime", gameTime, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[difficulty] The currently set campaign difficulty, or the current AI's difficulty setting. It will be one of
|
|
//== EASY, MEDIUM, HARD or INSANE.
|
|
if (game.type == SKIRMISH)
|
|
{
|
|
engine->globalObject().setProperty("difficulty", difficulty, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
}
|
|
else // campaign
|
|
{
|
|
engine->globalObject().setProperty("difficulty", (int)getDifficultyLevel(), QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
}
|
|
//== \item[mapName] The name of the current map.
|
|
engine->globalObject().setProperty("mapName", game.map, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[baseType] The area name of the map.
|
|
QString tilesetType("CUSTOM");
|
|
if (strcmp(tilesetDir, "texpages/tertilesc1hw") == 0)
|
|
{
|
|
tilesetType = "ARIZONA";
|
|
}
|
|
else if (strcmp(tilesetDir, "texpages/tertilesc2hw") == 0)
|
|
{
|
|
tilesetType = "URBAN";
|
|
}
|
|
else if (strcmp(tilesetDir, "texpages/tertilesc3hw") == 0)
|
|
{
|
|
tilesetType = "ROCKIES";
|
|
}
|
|
engine->globalObject().setProperty("tilesetType", tilesetType, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[baseType] The type of base that the game starts with. It will be one of CAMP_CLEAN, CAMP_BASE or CAMP_WALLS.
|
|
engine->globalObject().setProperty("baseType", game.base, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[alliancesType] The type of alliances permitted in this game. It will be one of NO_ALLIANCES, ALLIANCES or ALLIANCES_TEAMS.
|
|
engine->globalObject().setProperty("alliancesType", game.alliance, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[powerType] The power level set for this game.
|
|
engine->globalObject().setProperty("powerType", game.power, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[maxPlayers] The number of active players in this game.
|
|
engine->globalObject().setProperty("maxPlayers", game.maxPlayers, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[scavengers] Whether or not scavengers are activated in this game.
|
|
engine->globalObject().setProperty("scavengers", game.scavengers, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[mapWidth] Width of map in tiles.
|
|
engine->globalObject().setProperty("mapWidth", mapWidth, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[mapHeight] Height of map in tiles.
|
|
engine->globalObject().setProperty("mapHeight", mapHeight, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[scavengerPlayer] Index of scavenger player. (3.2+ only)
|
|
engine->globalObject().setProperty("scavengerPlayer", scavengerPlayer(), QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
//== \item[isMultiplayer] If the current game is a online multiplayer game or not. (3.2+ only)
|
|
engine->globalObject().setProperty("isMultiplayer", NetPlay.bComms, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
// un-documented placeholder variable
|
|
engine->globalObject().setProperty("isReceivingAllEvents", false, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
|
|
// Regular functions
|
|
QFileInfo basename(path);
|
|
registerFunctions(engine, basename.baseName());
|
|
|
|
// Remember internal, reserved names
|
|
QScriptValueIterator it(engine->globalObject());
|
|
while (it.hasNext())
|
|
{
|
|
it.next();
|
|
internalNamespace.insert(it.name(), 1);
|
|
}
|
|
|
|
// We need to always save the 'me' special variable.
|
|
//== \item[me] The player the script is currently running as.
|
|
engine->globalObject().setProperty("me", player, QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
|
|
// We also need to save the special 'scriptName' variable.
|
|
//== \item[scriptName] Base name of the script that is running.
|
|
engine->globalObject().setProperty("scriptName", basename.baseName(), QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
|
|
// We also need to save the special 'scriptPath' variable.
|
|
//== \item[scriptPath] Base path of the script that is running.
|
|
engine->globalObject().setProperty("scriptPath", basename.path(), QScriptValue::ReadOnly | QScriptValue::Undeletable);
|
|
|
|
QScriptValue result = engine->evaluate(source, path);
|
|
ASSERT_OR_RETURN(NULL, !engine->hasUncaughtException(), "Uncaught exception at line %d, file %s: %s",
|
|
engine->uncaughtExceptionLineNumber(), path.toUtf8().constData(), result.toString().toUtf8().constData());
|
|
|
|
// Register script
|
|
scripts.push_back(engine);
|
|
|
|
MONITOR *monitor = new MONITOR;
|
|
monitors.insert(engine, monitor);
|
|
|
|
debug(LOG_SAVE, "Created script engine %d for player %d from %s", scripts.size() - 1, player, path.toUtf8().constData());
|
|
return engine;
|
|
}
|
|
|
|
bool loadGlobalScript(QString path)
|
|
{
|
|
return loadPlayerScript(path, selectedPlayer, 0);
|
|
}
|
|
|
|
bool saveScriptStates(const char *filename)
|
|
{
|
|
WzConfig ini(filename);
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueIterator it(engine->globalObject());
|
|
ini.beginGroup(QString("globals_") + QString::number(i));
|
|
// we save 'scriptName' and 'me' implicitly
|
|
while (it.hasNext())
|
|
{
|
|
it.next();
|
|
if (!internalNamespace.contains(it.name()) && !it.value().isFunction())
|
|
{
|
|
ini.setValue(it.name(), it.value().toVariant());
|
|
}
|
|
}
|
|
ini.endGroup();
|
|
ini.beginGroup(QString("groups_") + QString::number(i));
|
|
// we have to save 'scriptName' and 'me' explicitly
|
|
ini.setValue("me", engine->globalObject().property("me").toInt32());
|
|
ini.setValue("scriptName", engine->globalObject().property("scriptName").toString());
|
|
saveGroups(ini, engine);
|
|
ini.endGroup();
|
|
}
|
|
for (int i = 0; i < timers.size(); ++i)
|
|
{
|
|
timerNode node = timers.at(i);
|
|
ini.beginGroup(QString("triggers_") + QString::number(i));
|
|
// we have to save 'scriptName' and 'me' explicitly
|
|
ini.setValue("me", node.player);
|
|
ini.setValue("scriptName", node.engine->globalObject().property("scriptName").toString());
|
|
ini.setValue("function", node.function);
|
|
if (node.baseobj >= 0)
|
|
{
|
|
ini.setValue("object", QVariant(node.baseobj));
|
|
}
|
|
ini.setValue("frame", node.frameTime);
|
|
ini.setValue("type", (int)node.type);
|
|
ini.setValue("ms", node.ms);
|
|
ini.endGroup();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static QScriptEngine *findEngineForPlayer(int match, QString scriptName)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
QString matchName = engine->globalObject().property("scriptName").toString();
|
|
if (match == player && (matchName.compare(scriptName, Qt::CaseInsensitive) == 0 || scriptName.isEmpty()))
|
|
{
|
|
return engine;
|
|
}
|
|
}
|
|
ASSERT(false, "Script context for player %d and script name %s not found",
|
|
match, scriptName.toUtf8().constData());
|
|
return NULL;
|
|
}
|
|
|
|
bool loadScriptStates(const char *filename)
|
|
{
|
|
WzConfig ini(filename, WzConfig::ReadOnly);
|
|
QStringList list = ini.childGroups();
|
|
debug(LOG_SAVE, "Loading script states for %d script contexts", scripts.size());
|
|
for (int i = 0; i < list.size(); ++i)
|
|
{
|
|
ini.beginGroup(list[i]);
|
|
int player = ini.value("me").toInt();
|
|
QString scriptName = ini.value("scriptName").toString();
|
|
QScriptEngine *engine = findEngineForPlayer(player, scriptName);
|
|
if (engine && list[i].startsWith("triggers_"))
|
|
{
|
|
timerNode node;
|
|
node.player = player;
|
|
node.ms = ini.value("ms").toInt();
|
|
node.frameTime = ini.value("frame").toInt();
|
|
node.engine = engine;
|
|
debug(LOG_SAVE, "Registering trigger %d for player %d, script %s",
|
|
i, node.player, scriptName.toUtf8().constData());
|
|
node.function = ini.value("function").toString();
|
|
node.baseobj = ini.value("baseobj", -1).toInt();
|
|
node.type = (timerType)ini.value("type", TIMER_REPEAT).toInt();
|
|
timers.push_back(node);
|
|
}
|
|
else if (engine && list[i].startsWith("globals_"))
|
|
{
|
|
QStringList keys = ini.childKeys();
|
|
debug(LOG_SAVE, "Loading script globals for player %d, script %s -- found %d values",
|
|
player, scriptName.toUtf8().constData(), keys.size());
|
|
for (int j = 0; j < keys.size(); ++j)
|
|
{
|
|
engine->globalObject().setProperty(keys.at(j), engine->toScriptValue(ini.value(keys.at(j))));
|
|
}
|
|
}
|
|
else if (engine && list[i].startsWith("groups_"))
|
|
{
|
|
QStringList keys = ini.childKeys();
|
|
for (int j = 0; j < keys.size(); ++j)
|
|
{
|
|
QStringList values = ini.value(keys.at(j)).toStringList();
|
|
bool ok = false; // check if number
|
|
int droidId = keys.at(j).toInt(&ok);
|
|
for (int k = 0; ok && k < values.size(); k++)
|
|
{
|
|
int groupId = values.at(k).toInt();
|
|
loadGroup(engine, groupId, droidId);
|
|
}
|
|
}
|
|
}
|
|
ini.endGroup();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static QStandardItemList addModelItem(QScriptValueIterator &it)
|
|
{
|
|
QStandardItemList l;
|
|
QStandardItem *key = new QStandardItem(it.name());
|
|
QStandardItem *value = NULL;
|
|
|
|
if (it.value().isObject() || it.value().isArray())
|
|
{
|
|
QScriptValueIterator obit(it.value());
|
|
while (obit.hasNext())
|
|
{
|
|
obit.next();
|
|
key->appendRow(addModelItem(obit));
|
|
}
|
|
value = new QStandardItem("[Object]");
|
|
}
|
|
else
|
|
{
|
|
value = new QStandardItem(it.value().toString());
|
|
}
|
|
l += key;
|
|
l += value;
|
|
return l;
|
|
}
|
|
|
|
static void updateGlobalModels()
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueIterator it(engine->globalObject());
|
|
QStandardItemModel *m = models.value(engine);
|
|
m->setRowCount(0);
|
|
|
|
while (it.hasNext())
|
|
{
|
|
it.next();
|
|
if ((!internalNamespace.contains(it.name()) && !it.value().isFunction())
|
|
|| it.name() == "Upgrades" || it.name() == "Stats")
|
|
{
|
|
QStandardItemList list = addModelItem(it);
|
|
m->appendRow(list);
|
|
}
|
|
}
|
|
}
|
|
QStandardItemModel *m = triggerModel;
|
|
m->setRowCount(0);
|
|
for (int i = 0; i < timers.size(); ++i)
|
|
{
|
|
timerNode node = timers.at(i);
|
|
int nextRow = m->rowCount();
|
|
m->setRowCount(nextRow);
|
|
m->setItem(nextRow, 0, new QStandardItem(node.function));
|
|
QString scriptName = node.engine->globalObject().property("scriptName").toString();
|
|
m->setItem(nextRow, 1, new QStandardItem(scriptName + ":" + QString::number(node.player)));
|
|
if (node.baseobj >= 0)
|
|
{
|
|
m->setItem(nextRow, 2, new QStandardItem(QString::number(node.baseobj)));
|
|
}
|
|
else
|
|
{
|
|
m->setItem(nextRow, 2, new QStandardItem("-"));
|
|
}
|
|
m->setItem(nextRow, 3, new QStandardItem(QString::number(node.frameTime)));
|
|
m->setItem(nextRow, 4, new QStandardItem(QString::number(node.ms)));
|
|
if (node.type == TIMER_ONESHOT_READY)
|
|
{
|
|
m->setItem(nextRow, 5, new QStandardItem("Oneshot"));
|
|
}
|
|
else if (node.type == TIMER_ONESHOT_DONE)
|
|
{
|
|
m->setItem(nextRow, 5, new QStandardItem("Done"));
|
|
}
|
|
else
|
|
{
|
|
m->setItem(nextRow, 5, new QStandardItem("Repeat"));
|
|
}
|
|
m->setItem(nextRow, 6, new QStandardItem(QString::number(node.calls)));
|
|
}
|
|
}
|
|
|
|
bool jsEvaluate(QScriptEngine *engine, const QString &text)
|
|
{
|
|
QScriptSyntaxCheckResult syntax = QScriptEngine::checkSyntax(text);
|
|
if (syntax.state() != QScriptSyntaxCheckResult::Valid)
|
|
{
|
|
debug(LOG_ERROR, "Syntax error in %s: %s",
|
|
text.toUtf8().constData(), syntax.errorMessage().toUtf8().constData());
|
|
return false;
|
|
}
|
|
QScriptValue result = engine->evaluate(text);
|
|
if (engine->hasUncaughtException())
|
|
{
|
|
debug(LOG_ERROR, "Uncaught exception in %s: %s",
|
|
text.toUtf8().constData(), result.toString().toUtf8().constData());
|
|
return false;
|
|
}
|
|
console("%s", result.toString().toUtf8().constData());
|
|
return true;
|
|
}
|
|
|
|
void jsAutogame()
|
|
{
|
|
QString srcPath(PHYSFS_getWriteDir());
|
|
srcPath += PHYSFS_getDirSeparator();
|
|
srcPath += "scripts";
|
|
QString path = QFileDialog::getOpenFileName(NULL, "Choose AI script to load", srcPath, "Javascript files (*.js)");
|
|
QFileInfo basename(path);
|
|
if (path.isEmpty())
|
|
{
|
|
console("No file specified");
|
|
return;
|
|
}
|
|
QScriptEngine *engine = loadPlayerScript("scripts/" + basename.fileName(), selectedPlayer, DIFFICULTY_MEDIUM);
|
|
if (!engine)
|
|
{
|
|
console("Failed to load selected AI! Check your logs to see why.");
|
|
return;
|
|
}
|
|
console("Loaded the %s AI script for current player!", path.toUtf8().constData());
|
|
callFunction(engine, "eventGameInit", QScriptValueList());
|
|
callFunction(engine, "eventStartLevel", QScriptValueList());
|
|
}
|
|
|
|
void jsShowDebug()
|
|
{
|
|
// Add globals
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QStandardItemModel *m = new QStandardItemModel(0, 2);
|
|
m->setHeaderData(0, Qt::Horizontal, QString("Name"));
|
|
m->setHeaderData(1, Qt::Horizontal, QString("Value"));
|
|
models.insert(engine, m);
|
|
}
|
|
// Add triggers
|
|
triggerModel = new QStandardItemModel(0, 7);
|
|
triggerModel->setHeaderData(0, Qt::Horizontal, QString("Function"));
|
|
triggerModel->setHeaderData(1, Qt::Horizontal, QString("Script"));
|
|
triggerModel->setHeaderData(2, Qt::Horizontal, QString("Object"));
|
|
triggerModel->setHeaderData(3, Qt::Horizontal, QString("Time"));
|
|
triggerModel->setHeaderData(4, Qt::Horizontal, QString("Interval"));
|
|
triggerModel->setHeaderData(5, Qt::Horizontal, QString("Type"));
|
|
triggerModel->setHeaderData(6, Qt::Horizontal, QString("Calls"));
|
|
|
|
globalDialog = true;
|
|
updateGlobalModels();
|
|
jsDebugCreate(models, triggerModel);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------------------
|
|
// Events
|
|
|
|
/// For generic, parameter-less triggers
|
|
//__ \subsection{eventGameInit()}
|
|
//__ An event that is run once as the game is initialized. Not all game may have been
|
|
//__ properly initialized by this time, so use this only to initialize script state.
|
|
//__ \subsection{eventStartLevel()}
|
|
//__ An event that is run once the game has started and all game data has been loaded.
|
|
//__ \subsection{eventMissionTimeout()}
|
|
//__ An event that is run when the mission timer has run out.
|
|
//__ \subsection{eventVideoDone()}
|
|
//__ An event that is run when a video show stopped playing.
|
|
//__ \subsection{eventGameLoaded()}
|
|
//__ An event that is run when game is loaded from a saved game. There is usually no need to use this event.
|
|
//__ \subsection{eventGameSaving()}
|
|
//__ An event that is run before game is saved. There is usually no need to use this event.
|
|
//__ \subsection{eventGameSaved()}
|
|
//__ An event that is run after game is saved. There is usually no need to use this event.
|
|
//__ \subsection{eventTransporterLaunch(transport)}
|
|
//__ An event that is run when the mission transporter has been ordered to fly off.
|
|
//__ \subsection{eventTransporterArrived(transport)}
|
|
//__ An event that is run when the mission transporter has arrived at the map edge with reinforcements.
|
|
//__ \subsection{eventTransporterExit(transport)}
|
|
//__ An event that is run when the mission transporter has left the map.
|
|
//__ \subsection{eventTransporterDone(transport)}
|
|
//__ An event that is run when the mission transporter has no more reinforcements to deliver.
|
|
//__ \subsection{eventTransporterLanded(transport)}
|
|
//__ An event that is run when the mission transporter has landed with reinforcements.
|
|
bool triggerEvent(SCRIPT_TRIGGER_TYPE trigger, BASE_OBJECT *psObj)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
|
|
if (psObj)
|
|
{
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (player != psObj->player && !receiveAll)
|
|
{
|
|
continue;
|
|
}
|
|
args += convMax(psObj, engine);
|
|
}
|
|
|
|
switch (trigger)
|
|
{
|
|
case TRIGGER_GAME_INIT:
|
|
callFunction(engine, "eventGameInit", QScriptValueList());
|
|
break;
|
|
case TRIGGER_START_LEVEL:
|
|
processVisibility(); // make sure we initialize visibility first
|
|
callFunction(engine, "eventStartLevel", QScriptValueList());
|
|
break;
|
|
case TRIGGER_TRANSPORTER_LAUNCH:
|
|
callFunction(engine, "eventLaunchTransporter", QScriptValueList()); // deprecated!
|
|
callFunction(engine, "eventTransporterLaunch", args);
|
|
break;
|
|
case TRIGGER_TRANSPORTER_ARRIVED:
|
|
callFunction(engine, "eventReinforcementsArrived", QScriptValueList()); // deprecated!
|
|
callFunction(engine, "eventTransporterArrived", args);
|
|
break;
|
|
case TRIGGER_OBJECT_RECYCLED:
|
|
callFunction(engine, "eventObjectRecycled", args);
|
|
break;
|
|
case TRIGGER_TRANSPORTER_EXIT:
|
|
callFunction(engine, "eventTransporterExit", args);
|
|
break;
|
|
case TRIGGER_TRANSPORTER_DONE:
|
|
callFunction(engine, "eventTransporterDone", args);
|
|
break;
|
|
case TRIGGER_TRANSPORTER_LANDED:
|
|
callFunction(engine, "eventTransporterLanded", args);
|
|
break;
|
|
case TRIGGER_MISSION_TIMEOUT:
|
|
callFunction(engine, "eventMissionTimeout", QScriptValueList());
|
|
break;
|
|
case TRIGGER_VIDEO_QUIT:
|
|
callFunction(engine, "eventVideoDone", QScriptValueList());
|
|
break;
|
|
case TRIGGER_GAME_LOADED:
|
|
callFunction(engine, "eventGameLoaded", QScriptValueList());
|
|
break;
|
|
case TRIGGER_GAME_SAVING:
|
|
callFunction(engine, "eventGameSaving", QScriptValueList());
|
|
break;
|
|
case TRIGGER_GAME_SAVED:
|
|
callFunction(engine, "eventGameSaved", QScriptValueList());
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventPlayerLeft(player index)}
|
|
//__ An event that is run after a player has left the game.
|
|
bool triggerEventPlayerLeft(int id)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += id;
|
|
callFunction(engine, "eventPlayerLeft", args);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventCheatMode(entered)} Game entered or left cheat/debug mode.
|
|
//__ The entered parameter is true if cheat mode entered, false otherwise.
|
|
bool triggerEventCheatMode(bool entered)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += entered;
|
|
callFunction(engine, "eventCheatMode", args);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventDroidIdle(droid)} A droid should be given new orders.
|
|
bool triggerEventDroidIdle(DROID *psDroid)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
if (player == psDroid->player)
|
|
{
|
|
QScriptValueList args;
|
|
args += convDroid(psDroid, engine);
|
|
callFunction(engine, "eventDroidIdle", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventDroidBuilt(droid[, structure])}
|
|
//__ An event that is run every time a droid is built. The structure parameter is set
|
|
//__ if the droid was produced in a factory. It is not triggered for droid theft or
|
|
//__ gift (check \emph{eventObjectTransfer} for that).
|
|
bool triggerEventDroidBuilt(DROID *psDroid, STRUCTURE *psFactory)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (player == psDroid->player || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += convDroid(psDroid, engine);
|
|
if (psFactory)
|
|
{
|
|
args += convStructure(psFactory, engine);
|
|
}
|
|
callFunction(engine, "eventDroidBuilt", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventStructureBuilt(structure[, droid])}
|
|
//__ An event that is run every time a structure is produced. The droid parameter is set
|
|
//__ if the structure was built by a droid. It is not triggered for building theft
|
|
//__ (check \emph{eventObjectTransfer} for that).
|
|
bool triggerEventStructBuilt(STRUCTURE *psStruct, DROID *psDroid)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (player == psStruct->player || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += convStructure(psStruct, engine);
|
|
if (psDroid)
|
|
{
|
|
args += convDroid(psDroid, engine);
|
|
}
|
|
callFunction(engine, "eventStructureBuilt", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventStructureReady(structure)}
|
|
//__ An event that is run every time a structure is ready to perform some
|
|
//__ special ability. It will only fire once, so if the time is not right,
|
|
//__ register your own timer to keep checking.
|
|
bool triggerEventStructureReady(STRUCTURE *psStruct)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (player == psStruct->player || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += convStructure(psStruct, engine);
|
|
callFunction(engine, "eventStructureReady", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventAttacked(victim, attacker)}
|
|
//__ An event that is run when an object belonging to the script's controlling player is
|
|
//__ attacked. The attacker parameter may be either a structure or a droid.
|
|
bool triggerEventAttacked(BASE_OBJECT *psVictim, BASE_OBJECT *psAttacker, int lastHit)
|
|
{
|
|
if (!psAttacker)
|
|
{
|
|
// do not fire off this event if there is no attacker -- nothing do respond to
|
|
// (FIXME -- consider this carefully)
|
|
return false;
|
|
}
|
|
// throttle the event for performance
|
|
if (gameTime - lastHit < ATTACK_THROTTLE)
|
|
{
|
|
return false;
|
|
}
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int player = engine->globalObject().property("me").toInt32();
|
|
if (player == psVictim->player)
|
|
{
|
|
QScriptValueList args;
|
|
args += convMax(psVictim, engine);
|
|
args += convMax(psAttacker, engine);
|
|
callFunction(engine, "eventAttacked", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventResearched(research, structure, player)}
|
|
//__ An event that is run whenever a new research is available. The structure
|
|
//__ parameter is set if the research comes from a research lab owned by the
|
|
//__ current player. If an ally does the research, the structure parameter will
|
|
//__ be set to null. The player parameter gives the player it is called for.
|
|
bool triggerEventResearched(RESEARCH *psResearch, STRUCTURE *psStruct, int player)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int me = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (me == player || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += convResearch(psResearch, engine, player);
|
|
if (psStruct)
|
|
{
|
|
args += convStructure(psStruct, engine);
|
|
}
|
|
else
|
|
{
|
|
args += QScriptValue::NullValue;
|
|
}
|
|
args += QScriptValue(player);
|
|
callFunction(engine, "eventResearched", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventDestroyed(object)}
|
|
//__ An event that is run whenever an object is destroyed. Careful passing
|
|
//__ the parameter object around, since it is about to vanish!
|
|
bool triggerEventDestroyed(BASE_OBJECT *psVictim)
|
|
{
|
|
for (int i = 0; i < scripts.size() && psVictim; ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += convMax(psVictim, engine);
|
|
callFunction(engine, "eventDestroyed", args);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventPickup(feature, droid)}
|
|
//__ An event that is run whenever a feature is picked up. It is called for
|
|
//__ all players / scripts.
|
|
//__ Careful passing the parameter object around, since it is about to vanish! (3.2+ only)
|
|
bool triggerEventPickup(FEATURE *psFeat, DROID *psDroid)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += convFeature(psFeat, engine);
|
|
args += convDroid(psDroid, engine);
|
|
callFunction(engine, "eventPickup", args);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventObjectSeen(viewer, seen)}
|
|
//__ An event that is run sometimes when an object goes from not seen to seen.
|
|
//__ First parameter is \emph{game object} doing the seeing, the next the game
|
|
//__ object being seen. This is event is throttled, and so is not called every time.
|
|
bool triggerEventSeen(BASE_OBJECT *psViewer, BASE_OBJECT *psSeen)
|
|
{
|
|
for (int i = 0; i < scripts.size() && psSeen && psViewer; ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int me = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (me == psViewer->player || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += convMax(psViewer, engine);
|
|
args += convMax(psSeen, engine);
|
|
callFunction(engine, "eventObjectSeen", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventObjectTransfer(object, from)}
|
|
//__ An event that is run whenever an object is transferred between players,
|
|
//__ for example due to a Nexus Link weapon. The event is called after the
|
|
//__ object has been transferred, so the target player is in object.player.
|
|
//__ The event is called for both players.
|
|
bool triggerEventObjectTransfer(BASE_OBJECT *psObj, int from)
|
|
{
|
|
for (int i = 0; i < scripts.size() && psObj; ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int me = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (me == psObj->player || me == from || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += convMax(psObj, engine);
|
|
args += QScriptValue(from);
|
|
callFunction(engine, "eventObjectTransfer", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventChat(from, to, message)}
|
|
//__ An event that is run whenever a chat message is received. The \emph{from} parameter is the
|
|
//__ player sending the chat message. For the moment, the \emph{to} parameter is always the script
|
|
//__ player.
|
|
bool triggerEventChat(int from, int to, const char *message)
|
|
{
|
|
for (int i = 0; i < scripts.size() && message; ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int me = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (me == to || (receiveAll && to == from))
|
|
{
|
|
QScriptValueList args;
|
|
args += QScriptValue(from);
|
|
args += QScriptValue(to);
|
|
args += QScriptValue(message);
|
|
callFunction(engine, "eventChat", args);
|
|
break; // only call once
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventBeacon(x, y, from, to[, message])}
|
|
//__ An event that is run whenever a beacon message is received. The \emph{from} parameter is the
|
|
//__ player sending the beacon. For the moment, the \emph{to} parameter is always the script player.
|
|
//__ Message may be undefined.
|
|
bool triggerEventBeacon(int from, int to, const char *message, int x, int y)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int me = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (me == to || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += QScriptValue(map_coord(x));
|
|
args += QScriptValue(map_coord(y));
|
|
args += QScriptValue(from);
|
|
args += QScriptValue(to);
|
|
if (message)
|
|
{
|
|
args += QScriptValue(message);
|
|
}
|
|
callFunction(engine, "eventBeacon", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventBeaconRemoved(from, to)}
|
|
//__ An event that is run whenever a beacon message is removed. The \emph{from} parameter is the
|
|
//__ player sending the beacon. For the moment, the \emph{to} parameter is always the script player.
|
|
bool triggerEventBeaconRemoved(int from, int to)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
int me = engine->globalObject().property("me").toInt32();
|
|
bool receiveAll = engine->globalObject().property("isReceivingAllEvents").toBool();
|
|
if (me == to || receiveAll)
|
|
{
|
|
QScriptValueList args;
|
|
args += QScriptValue(from);
|
|
args += QScriptValue(to);
|
|
callFunction(engine, "eventBeaconRemoved", args);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventSelectionChanged(objects)}
|
|
//__ An event that is triggered whenever the host player selects one or more game objects.
|
|
//__ The \emph{objects} parameter contains an array of the currently selected game objects.
|
|
//__ Keep in mind that the player may drag and drop select many units at once, select one
|
|
//__ unit specifically, or even add more selections to a current selection one at a time.
|
|
//__ This event will trigger once for each user action, not once for each selected or
|
|
//__ deselected object. If all selected game objects are deselected, \emph{objects} will
|
|
//__ be empty.
|
|
bool triggerEventSelected()
|
|
{
|
|
selectionChanged = true;
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventGroupLoss(object, group id, new size)}
|
|
//__ An event that is run whenever a group becomes empty. Input parameter
|
|
//__ is the about to be killed object, the group's id, and the new group size.
|
|
// Since groups are entities local to one context, we do not iterate over them here.
|
|
bool triggerEventGroupLoss(BASE_OBJECT *psObj, int group, int size, QScriptEngine *engine)
|
|
{
|
|
QScriptValueList args;
|
|
args += convMax(psObj, engine);
|
|
args += QScriptValue(group);
|
|
args += QScriptValue(size);
|
|
callFunction(engine, "eventGroupLoss", args);
|
|
return true;
|
|
}
|
|
|
|
// This is not a trigger yet.
|
|
bool triggerEventDroidMoved(DROID *psDroid, int oldx, int oldy)
|
|
{
|
|
return areaLabelCheck(psDroid);
|
|
}
|
|
|
|
//__ \subsection{eventArea<label>(droid)}
|
|
//__ An event that is run whenever a droid enters an area label. The area is then
|
|
//__ deactived. Call resetArea() to reactivate it. The name of the event is
|
|
//__ eventArea + the name of the label.
|
|
bool triggerEventArea(QString label, DROID *psDroid)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += QScriptValue(label);
|
|
args += convDroid(psDroid, engine);
|
|
QString funcname = QString("eventArea" + label);
|
|
debug(LOG_SCRIPT, "Triggering %s for %s", funcname.toUtf8().constData(),
|
|
engine->globalObject().property("scriptName").toString().toUtf8().constData());
|
|
callFunction(engine, funcname, args);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventDesignCreated(template)}
|
|
//__ An event that is run whenever a new droid template is created. It is only
|
|
//__ run on the client of the player designing the template.
|
|
bool triggerEventDesignCreated(DROID_TEMPLATE *psTemplate)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += convTemplate(psTemplate, engine);
|
|
callFunction(engine, "eventDesignCreated", args);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//__ \subsection{eventSyncRequest(req_id, x, y, obj_id, obj_id2)}
|
|
//__ An event that is called from a script and synchronized with all other scripts and hosts
|
|
//__ to prevent desync from happening. Sync requests must be carefully validated to prevent
|
|
//__ cheating!
|
|
bool triggerEventSyncRequest(int from, int req_id, int x, int y, BASE_OBJECT *psObj, BASE_OBJECT *psObj2)
|
|
{
|
|
for (int i = 0; i < scripts.size(); ++i)
|
|
{
|
|
QScriptEngine *engine = scripts.at(i);
|
|
QScriptValueList args;
|
|
args += QScriptValue(from);
|
|
args += QScriptValue(req_id);
|
|
args += QScriptValue(x);
|
|
args += QScriptValue(y);
|
|
if (psObj)
|
|
{
|
|
args += convMax(psObj, engine);
|
|
}
|
|
if (psObj2)
|
|
{
|
|
args += convMax(psObj2, engine);
|
|
}
|
|
callFunction(engine, "eventSyncRequest", args);
|
|
}
|
|
return true;
|
|
}
|