forked from 0ad/0ad
Run the AI in the same Compartment as the simulation. Let the AI access Sim data.
This is a paradigm change for AI computation. Historically, the AI was intended to be run in a separate thread from the simulation. The idea was that slow AI wouldn't stop the renderer from being smooth. In that original design, the AI received a copy of the game world and used that to run its logic. This meant the simulation could safely do whatever it wanted in the meantime. This copy was done via AIProxy & AIInterface. This design ended up having significant flaws: - The copying impacts the simulation negatively, particularly because AIProxy subscribes to a lot of messages (sometimes sent exclusively to it). This time cannot be threaded, and impacts MP games without AIs. - Copying the data is increasingly difficult. Modifiers are a headache, LOS is not implemented. Lots of logic is duplicated. The intended benefits of the design also failed to realise somewhat: - The AI was never threaded, and in fact, it is probably better to try and thread Sim + AI from the renderer than just the AI, at which point threading the AI specifically brings little benefit. The new design is much simpler and straighforward, but this has some side-effects: - The AI can now change the simulation. This can be used for cheating, or possibly for a tutorial AI. - The AI runs in the same GC zone as the simulation, which may lead to more frequent Sim GCs (but overall we might expect a reduction in temporary objects). - The AI state was essentially cached, so replacing some functions with Engine.QueryInterface might be slower. The tradeoff should be balanced by lower AIProxy computation times. Future work: - Threading some specific AI tasks could still be worthwhile, but should be done in specific worker threads, allowed to run over several turns if needed. Technical note: the AI 'global' is in its own Realm, which means name collisions with the same are not possible. Other notes: - The RL Interface uses the AI Interface and thus will gradually lose some data there. Given that the RL Interface can now request data however, this should be dine. Refs #5962, #2370 Differential Revision: https://code.wildfiregames.com/D3769 This was SVN commit r26274.
This commit is contained in:
parent
0779c64052
commit
4df03ed2d2
@ -590,6 +590,8 @@ m.Entity = m.Class({
|
||||
this._entityModif = sharedAI._entitiesModifications.get(entity.id);
|
||||
},
|
||||
|
||||
"queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid) },
|
||||
|
||||
"toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },
|
||||
|
||||
"id": function() { return this._entity.id; },
|
||||
@ -679,24 +681,25 @@ m.Entity = m.Class({
|
||||
},
|
||||
|
||||
"resourceSupplyAmount": function() {
|
||||
return this._entity.resourceSupplyAmount;
|
||||
return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount();
|
||||
},
|
||||
|
||||
"resourceSupplyNumGatherers": function()
|
||||
{
|
||||
return this._entity.resourceSupplyNumGatherers;
|
||||
return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers();
|
||||
},
|
||||
|
||||
"isFull": function()
|
||||
{
|
||||
if (this._entity.resourceSupplyNumGatherers !== undefined)
|
||||
return this.maxGatherers() === this._entity.resourceSupplyNumGatherers;
|
||||
let numGatherers = this.resourceSupplyNumGatherers();
|
||||
if (numGatherers)
|
||||
return this.maxGatherers() === numGatherers;
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
"resourceCarrying": function() {
|
||||
return this._entity.resourceCarrying;
|
||||
return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus();
|
||||
},
|
||||
|
||||
"currentGatherRate": function() {
|
||||
|
@ -179,27 +179,6 @@ AIProxy.prototype.OnGarrisonedUnitsChanged = function(msg)
|
||||
this.cmpAIInterface.PushEvent("UnGarrison", { "entity": ent, "holder": this.entity });
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnResourceSupplyChanged = function(msg)
|
||||
{
|
||||
if (!this.NotifyChange())
|
||||
return;
|
||||
this.changes.resourceSupplyAmount = msg.to;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnResourceSupplyNumGatherersChanged = function(msg)
|
||||
{
|
||||
if (!this.NotifyChange())
|
||||
return;
|
||||
this.changes.resourceSupplyNumGatherers = msg.to;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnResourceCarryingChanged = function(msg)
|
||||
{
|
||||
if (!this.NotifyChange())
|
||||
return;
|
||||
this.changes.resourceCarrying = msg.to;
|
||||
};
|
||||
|
||||
AIProxy.prototype.OnFoundationProgressChanged = function(msg)
|
||||
{
|
||||
if (!this.NotifyChange())
|
||||
@ -305,21 +284,6 @@ AIProxy.prototype.GetFullRepresentation = function()
|
||||
ret.foundationProgress = cmpFoundation.GetBuildPercentage();
|
||||
}
|
||||
|
||||
let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply);
|
||||
if (cmpResourceSupply)
|
||||
{
|
||||
// Updated by OnResourceSupplyChanged
|
||||
ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount();
|
||||
ret.resourceSupplyNumGatherers = cmpResourceSupply.GetNumGatherers();
|
||||
}
|
||||
|
||||
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
||||
if (cmpResourceGatherer)
|
||||
{
|
||||
// Updated by OnResourceCarryingChanged
|
||||
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
|
||||
}
|
||||
|
||||
let cmpResourceDropsite = Engine.QueryInterface(this.entity, IID_ResourceDropsite);
|
||||
if (cmpResourceDropsite)
|
||||
{
|
||||
|
@ -75,8 +75,6 @@ ResourceGatherer.prototype.GiveResources = function(resources)
|
||||
{
|
||||
for (let resource of resources)
|
||||
this.carrying[resource.type] = +resource.amount;
|
||||
|
||||
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -284,8 +282,6 @@ ResourceGatherer.prototype.PerformGather = function(data, lateness)
|
||||
if (cmpStatisticsTracker)
|
||||
cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific);
|
||||
|
||||
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
|
||||
|
||||
if (!this.CanCarryMore(type.generic))
|
||||
this.StopGathering("InventoryFilled");
|
||||
else if (status.exhausted)
|
||||
@ -399,17 +395,12 @@ ResourceGatherer.prototype.CommitResources = function(target)
|
||||
return;
|
||||
|
||||
let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity);
|
||||
let changed = false;
|
||||
for (let type in change)
|
||||
{
|
||||
this.carrying[type] -= change[type];
|
||||
if (this.carrying[type] == 0)
|
||||
delete this.carrying[type];
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -420,8 +411,6 @@ ResourceGatherer.prototype.CommitResources = function(target)
|
||||
ResourceGatherer.prototype.DropResources = function()
|
||||
{
|
||||
this.carrying = {};
|
||||
|
||||
Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -277,7 +277,6 @@ ResourceSupply.prototype.AddGatherer = function(gathererID)
|
||||
return true;
|
||||
|
||||
this.gatherers.push(gathererID);
|
||||
Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() });
|
||||
|
||||
return true;
|
||||
};
|
||||
@ -310,10 +309,7 @@ ResourceSupply.prototype.RemoveGatherer = function(gathererID)
|
||||
{
|
||||
let index = this.gatherers.indexOf(gathererID);
|
||||
if (index != -1)
|
||||
{
|
||||
this.gatherers.splice(index, 1);
|
||||
Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() });
|
||||
}
|
||||
|
||||
index = this.activeGatherers.indexOf(gathererID);
|
||||
if (index == -1)
|
||||
|
@ -1,7 +1 @@
|
||||
Engine.RegisterInterface("ResourceGatherer");
|
||||
|
||||
/**
|
||||
* Message of the form { "to": [{ "type": string, "amount": number, "max": number }] }
|
||||
* sent from ResourceGatherer component whenever the amount of carried resources changes.
|
||||
*/
|
||||
Engine.RegisterMessageType("ResourceCarryingChanged");
|
||||
|
@ -5,9 +5,3 @@ Engine.RegisterInterface("ResourceSupply");
|
||||
* sent from ResourceSupply component whenever the supply level changes.
|
||||
*/
|
||||
Engine.RegisterMessageType("ResourceSupplyChanged");
|
||||
|
||||
/**
|
||||
* Message of the form { "to": number }
|
||||
* sent from ResourceSupply component whenever the number of gatherer changes.
|
||||
*/
|
||||
Engine.RegisterMessageType("ResourceSupplyNumGatherersChanged");
|
||||
|
@ -51,7 +51,7 @@
|
||||
|
||||
struct ScriptInterface_impl
|
||||
{
|
||||
ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr<ScriptContext>& context);
|
||||
ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr<ScriptContext>& context, JS::Compartment* compartment);
|
||||
~ScriptInterface_impl();
|
||||
|
||||
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
|
||||
@ -299,7 +299,7 @@ bool ScriptInterface::Math_random(JSContext* cx, uint argc, JS::Value* vp)
|
||||
return true;
|
||||
}
|
||||
|
||||
ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr<ScriptContext>& context) :
|
||||
ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr<ScriptContext>& context, JS::Compartment* compartment) :
|
||||
m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext())
|
||||
{
|
||||
JS::RealmCreationOptions creationOpt;
|
||||
@ -307,6 +307,13 @@ ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const st
|
||||
creationOpt.setPreserveJitCode(true);
|
||||
// Enable uneval
|
||||
creationOpt.setToSourceEnabled(true);
|
||||
|
||||
if (compartment)
|
||||
creationOpt.setExistingCompartment(compartment);
|
||||
else
|
||||
// This is the default behaviour.
|
||||
creationOpt.setNewCompartmentAndZone();
|
||||
|
||||
JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{});
|
||||
|
||||
m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt);
|
||||
@ -340,7 +347,7 @@ ScriptInterface_impl::~ScriptInterface_impl()
|
||||
}
|
||||
|
||||
ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr<ScriptContext>& context) :
|
||||
m(std::make_unique<ScriptInterface_impl>(nativeScopeName, context))
|
||||
m(std::make_unique<ScriptInterface_impl>(nativeScopeName, context, nullptr))
|
||||
{
|
||||
// Profiler stats table isn't thread-safe, so only enable this on the main thread
|
||||
if (Threading::IsMainThread())
|
||||
@ -354,6 +361,24 @@ ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugN
|
||||
JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate);
|
||||
}
|
||||
|
||||
ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor)
|
||||
{
|
||||
ScriptRequest nrq(neighbor);
|
||||
JS::Compartment* comp = JS::GetCompartmentForRealm(JS::GetCurrentRealmOrNull(nrq.cx));
|
||||
m = std::make_unique<ScriptInterface_impl>(nativeScopeName, neighbor.GetContext(), comp);
|
||||
|
||||
// Profiler stats table isn't thread-safe, so only enable this on the main thread
|
||||
if (Threading::IsMainThread())
|
||||
{
|
||||
if (g_ScriptStatsTable)
|
||||
g_ScriptStatsTable->Add(this, debugName);
|
||||
}
|
||||
|
||||
ScriptRequest rq(this);
|
||||
m_CmptPrivate.pScriptInterface = this;
|
||||
JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate);
|
||||
}
|
||||
|
||||
ScriptInterface::~ScriptInterface()
|
||||
{
|
||||
if (Threading::IsMainThread())
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2022 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -85,6 +85,16 @@ public:
|
||||
*/
|
||||
ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr<ScriptContext>& context);
|
||||
|
||||
/**
|
||||
* Alternate constructor. This creates the new Realm in the same Compartment as the neighbor scriptInterface.
|
||||
* This means that data can be freely exchanged between these two script interfaces without cloning.
|
||||
* @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will
|
||||
* be placed into, as a scoping mechanism; typically "Engine"
|
||||
* @param debugName Name of this interface for CScriptStats purposes.
|
||||
* @param scriptInterface 'Neighbor' scriptInterface to share a compartment with.
|
||||
*/
|
||||
ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor);
|
||||
|
||||
~ScriptInterface();
|
||||
|
||||
struct CmptPrivate
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2022 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -510,11 +510,6 @@ void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationComman
|
||||
if (m_EnableOOSLog)
|
||||
DumpState();
|
||||
|
||||
// Start computing AI for the next turn
|
||||
CmpPtr<ICmpAIManager> cmpAIManager(m_SimContext, SYSTEM_ENTITY);
|
||||
if (cmpAIManager)
|
||||
cmpAIManager->StartComputation();
|
||||
|
||||
++m_TurnNumber;
|
||||
}
|
||||
|
||||
@ -535,10 +530,6 @@ void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengt
|
||||
componentManager.BroadcastMessage(msgTurnStart);
|
||||
}
|
||||
|
||||
// Push AI commands onto the queue before we use them
|
||||
CmpPtr<ICmpAIManager> cmpAIManager(simContext, SYSTEM_ENTITY);
|
||||
if (cmpAIManager)
|
||||
cmpAIManager->PushCommands();
|
||||
|
||||
CmpPtr<ICmpCommandQueue> cmpCommandQueue(simContext, SYSTEM_ENTITY);
|
||||
if (cmpCommandQueue)
|
||||
@ -583,6 +574,14 @@ void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengt
|
||||
// Clean up any entities destroyed during the simulation update
|
||||
componentManager.FlushDestroyedComponents();
|
||||
|
||||
// Compute AI immediately at turn's end.
|
||||
CmpPtr<ICmpAIManager> cmpAIManager(simContext, SYSTEM_ENTITY);
|
||||
if (cmpAIManager)
|
||||
{
|
||||
cmpAIManager->StartComputation();
|
||||
cmpAIManager->PushCommands();
|
||||
}
|
||||
|
||||
// Process all remaining moves
|
||||
if (cmpPathfinder)
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2022 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -57,27 +57,24 @@ extern void QuitEngine();
|
||||
* AI is primarily scripted, and the CCmpAIManager component defined here
|
||||
* takes care of managing all the scripts.
|
||||
*
|
||||
* To avoid slow AI scripts causing jerky rendering, they are run in a background
|
||||
* thread (maintained by CAIWorker) so that it's okay if they take a whole simulation
|
||||
* turn before returning their results (though preferably they shouldn't use nearly
|
||||
* that much CPU).
|
||||
* The original idea was to run CAIWorker in a separate thread to prevent
|
||||
* slow AIs from impacting framerate. However, copying the game-state every turn
|
||||
* proved difficult and rather slow itself (and isn't threadable, obviously).
|
||||
* For these reasons, the design was changed to a single-thread, same-compartment, different-realm design.
|
||||
* The AI can therefore directly use the simulation data via the 'Sim' & 'SimEngine' globals.
|
||||
* As a result, a lof of the code is still designed to be "thread-ready", but this no longer matters.
|
||||
*
|
||||
* CCmpAIManager grabs the world state after each turn (making use of AIInterface.js
|
||||
* and AIProxy.js to decide what data to include) then passes it to CAIWorker.
|
||||
* The AI scripts will then run asynchronously and return a list of commands to execute.
|
||||
* Any attempts to read the command list (including indirectly via serialization)
|
||||
* will block until it's actually completed, so the rest of the engine should avoid
|
||||
* reading it for as long as possible.
|
||||
* TODO: despite the above, it would still be useful to allow the AI to run tasks asynchronously (and off-thread).
|
||||
* This could be implemented by having a separate JS runtime in a different thread,
|
||||
* that runs tasks and returns after a distinct # of simulation turns (to maintain determinism).
|
||||
*
|
||||
* JS::Values are passed between the game and AI threads using Script::StructuredClone.
|
||||
*
|
||||
* TODO: actually the thread isn't implemented yet, because performance hasn't been
|
||||
* sufficiently problematic to justify the complexity yet, but the CAIWorker interface
|
||||
* is designed to hopefully support threading when we want it.
|
||||
* Note also that the RL Interface, by default, uses the 'AI representation'.
|
||||
* This representation, alimented by the JS AIInterface/AIProxy tandem, is likely to grow smaller over time
|
||||
* as the AI uses more sim data directly.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements worker thread for CCmpAIManager.
|
||||
* AI computation orchestator for CCmpAIManager.
|
||||
*/
|
||||
class CAIWorker
|
||||
{
|
||||
@ -206,27 +203,43 @@ private:
|
||||
std::shared_ptr<ScriptInterface> m_ScriptInterface;
|
||||
|
||||
JS::PersistentRootedValue m_Obj;
|
||||
std::vector<Script::StructuredClone > m_Commands;
|
||||
std::vector<Script::StructuredClone> m_Commands;
|
||||
};
|
||||
|
||||
public:
|
||||
struct SCommandSets
|
||||
{
|
||||
player_id_t player;
|
||||
std::vector<Script::StructuredClone > commands;
|
||||
std::vector<Script::StructuredClone> commands;
|
||||
};
|
||||
|
||||
CAIWorker() :
|
||||
m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptContext)),
|
||||
m_TurnNum(0),
|
||||
m_CommandsComputed(true),
|
||||
m_HasLoadedEntityTemplates(false),
|
||||
m_HasSharedComponent(false),
|
||||
m_EntityTemplates(g_ScriptContext->GetGeneralJSContext()),
|
||||
m_SharedAIObj(g_ScriptContext->GetGeneralJSContext()),
|
||||
m_PassabilityMapVal(g_ScriptContext->GetGeneralJSContext()),
|
||||
m_TerritoryMapVal(g_ScriptContext->GetGeneralJSContext())
|
||||
m_HasSharedComponent(false)
|
||||
{
|
||||
}
|
||||
|
||||
~CAIWorker()
|
||||
{
|
||||
// Init will always be called.
|
||||
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this);
|
||||
}
|
||||
|
||||
void Init(const ScriptInterface& simInterface)
|
||||
{
|
||||
// Create the script interface in the same compartment as the simulation interface.
|
||||
// This will allow us to directly share data from the sim to the AI (and vice versa, should the need arise).
|
||||
m_ScriptInterface = std::make_shared<ScriptInterface>("Engine", "AI", simInterface);
|
||||
|
||||
ScriptRequest rq(m_ScriptInterface);
|
||||
|
||||
m_EntityTemplates.init(rq.cx);
|
||||
m_SharedAIObj.init(rq.cx);
|
||||
m_PassabilityMapVal.init(rq.cx);
|
||||
m_TerritoryMapVal.init(rq.cx);
|
||||
|
||||
|
||||
m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG);
|
||||
|
||||
@ -234,7 +247,15 @@ public:
|
||||
|
||||
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this);
|
||||
|
||||
ScriptRequest rq(m_ScriptInterface);
|
||||
{
|
||||
ScriptRequest simrq(simInterface);
|
||||
// Register the sim globals for easy & explicit access. Mark it replaceable for hotloading.
|
||||
JS::RootedValue global(rq.cx, simrq.globalValue());
|
||||
m_ScriptInterface->SetGlobal("Sim", global, true);
|
||||
JS::RootedValue scope(rq.cx, JS::ObjectValue(*simrq.nativeScope.get()));
|
||||
m_ScriptInterface->SetGlobal("SimEngine", scope, true);
|
||||
}
|
||||
|
||||
#define REGISTER_FUNC_NAME(func, name) \
|
||||
ScriptFunction::Register<&CAIWorker::func, ScriptInterface::ObjectFromCBData<CAIWorker>>(rq, name);
|
||||
|
||||
@ -253,11 +274,7 @@ public:
|
||||
|
||||
// Globalscripts may use VFS script functions
|
||||
m_ScriptInterface->LoadGlobalScripts();
|
||||
}
|
||||
|
||||
~CAIWorker()
|
||||
{
|
||||
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this);
|
||||
}
|
||||
|
||||
bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; }
|
||||
@ -814,10 +831,6 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
|
||||
// members have to be called before the context destructor.
|
||||
std::shared_ptr<ScriptContext> m_ScriptContext;
|
||||
|
||||
std::shared_ptr<ScriptInterface> m_ScriptInterface;
|
||||
boost::rand48 m_RNG;
|
||||
u32 m_TurnNum;
|
||||
@ -870,6 +883,8 @@ public:
|
||||
|
||||
virtual void Init(const CParamNode& UNUSED(paramNode))
|
||||
{
|
||||
m_Worker.Init(GetSimContext().GetScriptInterface());
|
||||
|
||||
m_TerritoriesDirtyID = 0;
|
||||
m_TerritoriesDirtyBlinkingID = 0;
|
||||
m_JustDeserialized = false;
|
||||
|
Loading…
Reference in New Issue
Block a user