From f378a63d941d17f44b94f5b42750f644bc6a0f18 Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Mon, 22 Nov 2010 20:12:04 +0000 Subject: [PATCH] Add some tests for UnitAI. Fix said tests for UnitAI. Hopefully fix #647 too. Document HFSM interface a bit. Add Engine.DumpSimState() console command for debugging. This was SVN commit r8681. --- .../public/simulation/components/UnitAI.js | 25 ++-- .../simulation/components/tests/setup.js | 30 +++- .../components/tests/test_UnitAI.js | 130 ++++++++++++++++++ .../mods/public/simulation/helpers/FSM.js | 67 +++++++++ source/gui/scripting/ScriptFunctions.cpp | 8 ++ .../components/tests/test_scripts.h | 7 + 6 files changed, 257 insertions(+), 10 deletions(-) create mode 100644 binaries/data/mods/public/simulation/components/tests/test_UnitAI.js diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 457db73172..7be6560448 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -235,9 +235,6 @@ var UnitFsmSpec = { }, "IDLE": { - "enter": function() { - this.SelectAnimation("idle"); - }, }, "WALKING": { @@ -261,6 +258,11 @@ var UnitFsmSpec = { "FORMATIONMEMBER": { "FormationLeave": function(msg) { + // We're leaving the formation, so stop our FormationWalk order + if (this.FinishOrder()) + return; + + // No orders left, we're an individual now this.SetNextState("INDIVIDUAL.IDLE"); }, @@ -294,6 +296,10 @@ var UnitFsmSpec = { "IDLE": { "enter": function() { + // Switch back to idle animation to guarantee we won't + // get stuck with an incorrect animation + this.SelectAnimation("idle"); + // If we entered the idle state we must have nothing better to do, // so immediately check whether there's anybody nearby to attack. // (If anyone approaches later, it'll be handled via LosRangeUpdate.) @@ -302,11 +308,10 @@ var UnitFsmSpec = { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var ents = rangeMan.ResetActiveQuery(this.losRangeQuery); if (this.GetStance().attackOnSight && this.AttackVisibleEntity(ents)) - return true; + return true; // (abort the transition since we may have already switched state) } - // Nobody to attack - switch to idle - this.SelectAnimation("idle"); + // Nobody to attack - stay in idle return false; }, @@ -728,8 +733,9 @@ UnitAI.prototype.SetupRangeQuery = function(owner) var players = []; - if(owner != -1) - { // If unit not just killed, get enemy players via diplomacy + if (owner != -1) + { + // If unit not just killed, get enemy players via diplomacy var player = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); // Get our diplomacy array @@ -737,7 +743,8 @@ UnitAI.prototype.SetupRangeQuery = function(owner) var numPlayers = playerMan.GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) - { // Exclude gaia, allies, and self + { + // Exclude gaia, allies, and self // TODO: How to handle neutral players - Special query to attack military only? if (i != owner && diplomacy[i - 1] < 0) players.push(i); diff --git a/binaries/data/mods/public/simulation/components/tests/setup.js b/binaries/data/mods/public/simulation/components/tests/setup.js index 99009b4150..fd47b14b54 100644 --- a/binaries/data/mods/public/simulation/components/tests/setup.js +++ b/binaries/data/mods/public/simulation/components/tests/setup.js @@ -28,11 +28,34 @@ Engine.QueryInterface = function(ent, iid) return null; }; +Engine.RegisterGlobal = function(name, value) +{ + global[name] = value; +}; + +Engine.DestroyEntity = function(ent) +{ + for (var cid in g_Components[ent]) + { + var cmp = g_Components[ent][cid]; + if (cmp.Deinit) + cmp.Deinit(); + } + + delete g_Components[ent]; + + // TODO: should send Destroy message +}; + // TODO: -// Engine.RegisterGlobal // Engine.PostMessage // Engine.BroadcastMessage +global.ResetState = function() +{ + g_Components = {}; +}; + global.AddMock = function(ent, iid, mock) { if (!g_Components[ent]) @@ -46,5 +69,10 @@ global.ConstructComponent = function(ent, name, template) cmp.entity = ent; cmp.template = template; cmp.Init(); + + if (!g_Components[ent]) + g_Components[ent] = {}; + g_Components[ent][g_ComponentTypes[name].iid] = cmp; + return cmp; }; diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js new file mode 100644 index 0000000000..522485277d --- /dev/null +++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -0,0 +1,130 @@ +Engine.LoadHelperScript("FSM.js"); +Engine.LoadComponentScript("interfaces/Attack.js"); +Engine.LoadComponentScript("interfaces/DamageReceiver.js"); +Engine.LoadComponentScript("interfaces/Formation.js"); +Engine.LoadComponentScript("interfaces/Health.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("interfaces/UnitAI.js"); +Engine.LoadComponentScript("Formation.js"); +Engine.LoadComponentScript("UnitAI.js"); + +/* Regression test. + * Tests the FSM behaviour of a unit when walking as part of a formation, + * then exiting the formation. + * mode == 0: There is no enemy unit nearby. + * mode == 1: There is a live enemy unit nearby. + * mode == 2: There is a dead enemy unit nearby. + */ +function TestFormationExiting(mode) +{ + ResetState(); + + var playerEntity = 5; + var unit = 10; + var enemy = 20; + var controller = 30; + + + AddMock(SYSTEM_ENTITY, IID_Timer, { + SetTimeout: function() { }, + }); + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + CreateActiveQuery: function(ent, minRange, maxRange, players, iid) { + return 1; + }, + EnableActiveQuery: function(id) { }, + ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; }, + DisableActiveQuery: function(id) { }, + }); + + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + GetPlayerByID: function(id) { return playerEntity; }, + GetNumPlayers: function() { return 2; }, + }); + + AddMock(playerEntity, IID_Player, { + GetDiplomacy: function() { return []; }, + }); + + + var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false" }); + + AddMock(unit, IID_Position, { + GetPosition: function() { return { "x": 0, "z": 0 }; }, + IsInWorld: function() { return true; }, + }); + + AddMock(unit, IID_UnitMotion, { + GetWalkSpeed: function() { return 1; }, + MoveToFormationOffset: function(target, x, z) { }, + MoveToAttackRange: function(target, min, max) { }, + }); + + AddMock(unit, IID_Vision, { + GetRange: function() { return 10; }, + }); + + AddMock(unit, IID_Attack, { + GetRange: function() { return 10; }, + GetBestAttack: function() { return "melee"; }, + GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, + }); + + unitAI.OnCreate(); + + unitAI.SetupRangeQuery(1); + + + if (mode == 1) + AddMock(enemy, IID_Health, { + GetHitpoints: function() { return 10; }, + }); + else if (mode == 2) + AddMock(enemy, IID_Health, { + GetHitpoints: function() { return 0; }, + }); + + var controllerFormation = ConstructComponent(controller, "Formation"); + var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true" }); + + AddMock(controller, IID_Position, { + JumpTo: function(x, z) { this.x = x; this.z = z; }, + GetPosition: function() { return { "x": this.x, "z": this.z }; }, + IsInWorld: function() { return true; }, + }); + + AddMock(controller, IID_UnitMotion, { + SetUnitRadius: function(r) { }, + SetSpeed: function(speed) { }, + MoveToPoint: function(x, z) { }, + }); + + controllerAI.OnCreate(); + + + TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE"); + TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); + + controllerFormation.SetMembers([unit]); + controllerAI.Walk(100, 100, false); + controllerAI.OnMotionChanged({ "starting": true }); + + TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING"); + TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING"); + + controllerFormation.Disband(); + + if (mode == 0) + TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); + else if (mode == 1) + TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + else if (mode == 2) + TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); + else + TS_FAIL("invalid mode"); +} + +TestFormationExiting(0); +TestFormationExiting(1); +TestFormationExiting(2); diff --git a/binaries/data/mods/public/simulation/helpers/FSM.js b/binaries/data/mods/public/simulation/helpers/FSM.js index f28757678e..27539d9e5f 100644 --- a/binaries/data/mods/public/simulation/helpers/FSM.js +++ b/binaries/data/mods/public/simulation/helpers/FSM.js @@ -9,6 +9,73 @@ // plain old JS objects, with no need to serialise the complex // FSM structure itself or to add custom serialisation code.) +/** + +FSM API: + +Users define the FSM behaviour like: + +var FsmSpec = { + + // Define some default message handlers: + + "MessageName1": function(msg) { + // This function will be called in response to calls to + // Fsm.ProcessMessage(this, { "type": "MessageName1", "data": msg }); + // + // In this function, 'this' is the component object passed into + // ProcessMessage, so you can access 'this.propertyName' + // and 'this.methodName()' etc. + }, + + "MessageName2": function(msg) { + // Another message handler. + }, + + // Define the behaviour for the 'STATENAME' state: + "STATENAME": { + + "MessageName1": function(msg) { + // This overrides the previous MessageName1 that was + // defined earlier, and will be called instead of it + // in response to ProcessMessage. + }, + + // We don't override MessageName2, so the default one + // will be called instead. + + // Define the 'STATENAME.SUBSTATENAME' state: + // (we support arbitrarily-nested hierarchies of states) + "SUBSTATENAME": { + + "MessageName2": function(msg) { + // Override the default MessageName2. + // But we don't override MessageName1, so the one from + // STATENAME will be used instead. + }, + + "enter": function() { + // This is a special function called when transitioning + // into this state, or into a substate of this state. + // + // If it returns true, the transition will be aborted: + // do this if you've called SetNextState inside this enter + // handler, because otherwise the new state transition + // will get mixed up with the previous ongoing one. + // In normal cases, you can return false or nothing. + }, + + "leave": function() { + // Called when transitioning out of this state. + }, + }, + + } + +} + + */ + function FSM(spec) { // The (relatively) human-readable FSM specification needs to get diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp index 48824e8966..d03c039152 100644 --- a/source/gui/scripting/ScriptFunctions.cpp +++ b/source/gui/scripting/ScriptFunctions.cpp @@ -379,6 +379,13 @@ void ForceGC(void* cbdata) g_Console->InsertMessage(L"Garbage collection completed in: %f", time); } +void DumpSimState(void* UNUSED(cbdata)) +{ + fs::wpath path (psLogDir()/L"sim_dump.txt"); + std::ofstream file (path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc); + g_Game->GetSimulation2()->DumpDebugState(file); +} + } // namespace void GuiScriptingInit(ScriptInterface& scriptInterface) @@ -430,4 +437,5 @@ void GuiScriptingInit(ScriptInterface& scriptInterface) scriptInterface.RegisterFunction("Crash"); scriptInterface.RegisterFunction("DebugWarn"); scriptInterface.RegisterFunction("ForceGC"); + scriptInterface.RegisterFunction("DumpSimState"); } diff --git a/source/simulation2/components/tests/test_scripts.h b/source/simulation2/components/tests/test_scripts.h index bfd06dc38b..406e0697c9 100644 --- a/source/simulation2/components/tests/test_scripts.h +++ b/source/simulation2/components/tests/test_scripts.h @@ -51,6 +51,12 @@ public: TS_ASSERT(componentManager->LoadScript(L"simulation/components/"+name)); } + static void Script_LoadHelperScript(void* cbdata, std::wstring name) + { + CComponentManager* componentManager = static_cast (cbdata); + TS_ASSERT(componentManager->LoadScript(L"simulation/helpers/"+name)); + } + void test_scripts() { if (!FileExists(L"simulation/components/tests/setup.js")) @@ -69,6 +75,7 @@ public: ScriptTestSetup(componentManager.GetScriptInterface()); componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); + componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); componentManager.LoadComponentTypes();