1
0
forked from 0ad/0ad

Add engine support to triggers + a component to provide an easy interface and to be extended by triggers. Thanks to SpahBod for the biggest part of the code, and Yves for the review.

This was SVN commit r15421.
This commit is contained in:
sanderd17 2014-06-23 13:42:59 +00:00
parent 213732d5af
commit 9f47ed536d
21 changed files with 450 additions and 0 deletions

View File

@ -0,0 +1,24 @@
/**
* returns a clone of a simple object or array
* Only valid JSON objects are accepted
* So no recursion, and only plain obects or arrays
*/
function clone(o)
{
if (o instanceof Array)
var r = [];
else if (o instanceof Object)
var r = {};
else // native data type
return o;
for (var key in o)
r[key] = clone(o[key]);
return r;
}
var data = { "sub1": { "sub1prop1":"original" }, "prop1": "original", "prop2": [1,2,3] };
var newData = clone(data);
newData["sub1"]["sub1prop1"] = "modified";
print("data" + uneval(data));
print("newData" + uneval(newData));

View File

@ -202,6 +202,10 @@ Foundation.prototype.Build = function(builderEnt, work)
// (via CCmpTemplateManager). Now we need to remove that temporary
// blocker-disabling, so that we'll perform standard unit blocking instead.
cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ConstructionStarted", {"foundation": this.entity, "template": this.finalTemplateName});
}
// Switch foundation to scaffold variant

View File

@ -288,6 +288,10 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("TrainingQueued", {"playerid": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity});
}
else if (type == "technology")
{
@ -324,6 +328,10 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ResearchQueued", {"playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity});
}
else
{

View File

@ -348,6 +348,10 @@ TechnologyManager.prototype.ResearchTechnology = function (tech)
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = cmpRangeManager.GetEntitiesByPlayer(playerID);
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ResearchFinished", {"player": playerID, "tech": tech});
for (var component in modifiedComponents)
{
Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]});

View File

@ -0,0 +1,268 @@
function Trigger() {};
Trigger.prototype.Schema =
"<a:component type='system'/><empty/>";
/**
* Events we're able to receive and call handlers for
*/
Trigger.prototype.eventNames =
[
"StructureBuilt",
"ConstructionStarted",
"TrainingFinished",
"TrainingQueued",
"ResearchFinished",
"ResearchQueued",
"PlayerCommand",
"Interval",
"Range",
];
Trigger.prototype.Init = function()
{
this.triggerPoints = {};
// Each event has its own set of actions determined by the map maker.
for each (var eventName in this.eventNames)
this["On" + eventName + "Actions"] = {};
// To prevent the lose of trigger variables after a save, they "should" be defined in "InitTriggers" function, which is the starting point of a trigger script
this.DoAfterDelay(0, "InitGame", {});
};
/**
* This method may be overwritten by triggers
*/
Trigger.prototype.InitGame = function()
{
};
Trigger.prototype.RegisterTriggerPoint = function(ref, ent)
{
if (!this.triggerPoints[ref])
this.triggerPoints[ref] = [];
this.triggerPoints[ref].push(ent);
};
Trigger.prototype.RemoveRegisteredTriggerPoint = function(ref, ent)
{
if (!this.triggerPoints[ref])
{
warn("no trigger points found with ref "+ref);
return;
}
var i = this.triggerPoints[ref].indexOf(ent);
if (i == -1)
{
warn("entity " + ent + " wasn't found under the trigger points with ref "+ref);
return;
}
this.triggerPoints[ref].splice(i, 1);
};
Trigger.prototype.GetTriggerPoints = function(ref)
{
return this.triggerPoints[ref] || [];
};
/** Binds the "action" function to one of the implemented events.
*
* @param event Name of the event (see the list in init)
* @param action Functionname of a function available under this object
* @param Extra Data for the trigger (enabled or not, delay for timers, range for range triggers ...)
*
* Interval triggers example:
* data = {enabled: true, interval: 1000, delay: 500}
*
* Range trigger:
* data.entities = [id1, id2] * Ids of the source
* data.players = [1,2,3,...] * list of player ids
* data.minRange = 0 * Minimum range for the query
* data.maxRange = -1 * Maximum range for the query (-1 = no maximum)
* data.requiredComponent = 0 * Required component id the entities will have
* data.enabled = false * If the query is enabled by default
*/
Trigger.prototype.RegisterTrigger = function(event, action, data)
{
var eventString = event + "Actions";
if (!this[eventString])
{
warn("Trigger.js: Invalid trigger event \"" + event + "\".")
return;
}
if (this[eventString][action])
{
warn("Trigger.js: Trigger has been registered before. Aborting...");
return;
}
// clone the data to be sure it's only modified locally
// We could run into triggers overwriting each other's data otherwise.
// F.e. getting the wrong timer tag
data = clone(data) || {"enabled": false};
this[eventString][action] = data;
// setup range query
if (event == "OnRange")
{
if (!data.entities)
{
warn("Trigger.js: Range triggers should carry extra data");
return;
}
data.queries = [];
for (var ent of data.entities)
{
var cmpTriggerPoint = Engine.QueryInterface(ent, IID_TriggerPoint);
if (!cmpTriggerPoint)
{
warn("Trigger.js: Range triggers must be defined on trigger points");
continue;
}
data.queries.push(cmpTriggerPoint.RegisterRangeTrigger(action, data));
}
}
if (data.enabled)
this.EnableTrigger(event, action);
};
// Disable trigger
Trigger.prototype.DisableTrigger = function(event, action)
{
var eventString = event + "Actions";
if (!this[eventString][action])
{
warn("Trigger.js: Disabling unknown trigger");
return;
}
var data = this[eventString][action];
// special casing interval and range triggers for performance
if (event == "OnInterval")
{
if (!data.timer) // don't disable it a second time
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(data.timer);
data.timer = null;
}
else if (event == "OnRange")
{
if (!data.queries)
{
warn("Trigger.js: Range query wasn't set up before trying to disable it.");
return;
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (var query of data.queries)
cmpRangeManager.DisableActiveQuery(query);
}
data.enabled = false;
};
// Enable trigger
Trigger.prototype.EnableTrigger = function(event, action)
{
var eventString = event + "Actions";
if (!this[eventString][action])
{
warn("Trigger.js: Enabling unknown trigger");
return;
}
var data = this[eventString][action];
// special casing interval and range triggers for performance
if (event == "OnInterval")
{
if (data.timer) // don't enable it a second time
return;
if (!data.interval)
{
warn("Trigger.js: An interval trigger should have an intervel in its data")
return;
}
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var timer = cmpTimer.SetInterval(this.entity, IID_Trigger, "DoAction", data.delay || 0, data.interval, {"action" : action});
data.timer = timer;
}
else if (event == "OnRange")
{
if (!data.queries)
{
warn("Trigger.js: Range query wasn't set up before");
return;
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (var query of data.queries)
cmpRangeManager.EnableActiveQuery(query);
}
data.enabled = true;
};
/**
* This function executes the actions bound to the events
* It's either called directlty from other simulation scripts,
* or from message listeners in this file
*
* @param event Name of the event (see the list in init)
* @param data Data object that will be passed to the actions
*/
Trigger.prototype.CallEvent = function(event, data)
{
var eventString = "On" + event + "Actions";
if (!this[eventString])
{
warn("Trigger.js: Unknown trigger event called:\"" + event + "\".");
return;
}
for (var action in this[eventString])
{
if (this[eventString][action].enabled)
this.DoAction({"action": action, "data":data});
}
};
// Handles "OnStructureBuilt" event.
Trigger.prototype.OnGlobalConstructionFinished = function(msg)
{
this.CallEvent("StructureBuilt", {"building": msg.newentity}); // The data for this one is {"building": constructedBuilding}
};
// Handles "OnTrainingFinished" event.
Trigger.prototype.OnGlobalTrainingFinished = function(msg)
{
this.CallEvent("TrainingFinished", msg);
// The data for this one is {"entities": createdEnts,
// "owner": cmpOwnership.GetOwner(),
// "metadata": metadata}
// See function "SpawnUnits" in ProductionQueue for more details
};
/**
* Execute a function after a certain delay
* @param time The delay expressed in milleseconds
* @param action Name of the action function
* @param data Data object that will be passed to the action function
*/
Trigger.prototype.DoAfterDelay = function(miliseconds, action, data)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
return cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "DoAction", miliseconds, {"action": action, "data": data});
};
/**
* Called by the trigger listeners to exucute the actual action. Including sanity checks.
*/
Trigger.prototype.DoAction = function(msg)
{
if (this[msg.action])
this[msg.action](msg.data || null);
else
warn("Trigger.js: called a trigger action '" + msg.action + "' that wasn't found");
};
Engine.RegisterSystemComponentType(IID_Trigger, "Trigger", Trigger);

View File

@ -0,0 +1,88 @@
function TriggerPoint() {};
TriggerPoint.prototype.Schema =
"<optional>" +
"<element name='Reference'>" +
"<text/>" +
"</element>" +
"</optional>";
TriggerPoint.prototype.Init = function()
{
if (this.template && this.template.Reference)
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.RegisterTriggerPoint(this.template.Reference, this.entity);
}
this.currentCollections = {};
this.actions = {};
};
TriggerPoint.prototype.OnDestroy = function()
{
if (this.template && this.template.EntityReference)
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.RemoveRegisteredTriggerPoint(this.template.Reference, this.entity);
}
};
/**
* @param action Name of the action function defined under Trigger
* @param data The data is an object containing information for the range query
* Some of the data has sendible defaults (mentionned next to the object)
* data.players = [1,2,3,...] * list of player ids
* data.minRange = 0 * Minimum range for the query
* data.maxRange = -1 * Maximum range for the query (-1 = no maximum)
* data.requiredComponent = -1 * Required component id the entities will have
* data.enabled = false * If the query is enabled by default
*/
TriggerPoint.prototype.RegisterRangeTrigger = function(action, data)
{
if (data.players)
var players = data.players;
else
{
var numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
var players = [];
for (var i = 0; i < numPlayers; i++)
players.push(i);
}
var minRange = data.minRange || 0;
var maxRange = data.maxRange || -1;
var cid = data.requiredComponent || -1;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var tag = cmpRangeManager.CreateActiveQuery(this.entity, minRange, maxRange, players, cid, cmpRangeManager.GetEntityFlagMask("normal"));
this.currentCollections[tag] = [];
this.actions[tag] = action;
return tag;
};
TriggerPoint.prototype.OnRangeUpdate = function(msg)
{
var collection = this.currentCollections[msg.tag];
if (!collection)
return;
for (var ent of msg.removed)
{
var index = collection.indexOf(ent);
if (index > -1)
collection.splice(index, 1);
}
for each (var entity in msg.added)
collection.push(entity);
var r = {"currentCollection": collection.slice()};
r.added = msg.added;
r.removed = msg.removed;
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.DoAction({"action":this.actions[msg.tag], "data": r});
};
Engine.RegisterComponentType(IID_TriggerPoint, "TriggerPoint", TriggerPoint);

View File

@ -0,0 +1 @@
Engine.RegisterInterface("Trigger");

View File

@ -0,0 +1,2 @@
Engine.RegisterInterface("TriggerPoint");

View File

@ -31,7 +31,12 @@ function ProcessCommand(player, cmd)
// Now handle various commands
if (commands[cmd.type])
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (cmpTrigger)
cmpTrigger.CallEvent("PlayerCommand", {"player": player, "cmd": cmd});
commands[cmd.type](player, cmd, data);
}
else
error("Invalid command: unknown command type: "+uneval(cmd));
}

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>A</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_a.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>B</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_b.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>C</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_c.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>D</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_d.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>E</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_e.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>F</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_f.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>G</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_g.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>H</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_h.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>I</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_i.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>J</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_j.xml</Actor>
</VisualActor>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_trigger_point">
<TriggerPoint>
<Reference>K</Reference>
</TriggerPoint>
<VisualActor>
<Actor>props/special/common/marker_object_char_k.xml</Actor>
</VisualActor>

View File

@ -702,6 +702,19 @@ void CSimulation2::LoadMapSettings()
if (!m->m_StartupScript.empty())
GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript);
// Load the trigger script after we have loaded the simulation and the map.
if (GetScriptInterface().HasProperty(m->m_MapSettings.get(), "TriggerScripts"))
{
std::vector<std::string> scriptNames;
GetScriptInterface().GetProperty(m->m_MapSettings.get(), "TriggerScripts", scriptNames);
for (u32 i = 0; i < scriptNames.size(); i++)
{
std::string scriptName = "maps/" + scriptNames[i];
LOGMESSAGE(L"Loading trigger script '%hs'", scriptName.c_str());
m->m_ComponentManager.LoadScript(scriptName.data());
}
}
}
int CSimulation2::ProgressiveLoad()