Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -348,6 +348,7 @@ enabletips = true ; Enable/Disable tips during gamesetup (for newcomers) assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled. aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest) +aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive) [gui.session] camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one? Index: binaries/data/mods/public/gui/aiconfig/aiconfig.js =================================================================== --- binaries/data/mods/public/gui/aiconfig/aiconfig.js +++ binaries/data/mods/public/gui/aiconfig/aiconfig.js @@ -1,6 +1,6 @@ var g_PlayerSlot; -const g_AIDescriptions = [{ +var g_AIDescriptions = [{ "id": "", "data": { "name": translateWithContext("ai", "None"), @@ -8,28 +8,39 @@ } }].concat(g_Settings.AIDescriptions); +var g_AIControls = { + "aiSelection": { + "labels": g_AIDescriptions.map(ai => ai.data.name), + "selected": settings => g_AIDescriptions.findIndex(ai => ai.id == settings.id) + }, + "aiDifficulty": { + "labels": prepareForDropdown(g_Settings.AIDifficulties).Title, + "selected": settings => settings.difficulty + }, + "aiBehavior": { + "labels": prepareForDropdown(g_Settings.AIBehaviors).Title, + "selected": settings => g_Settings.AIBehaviors.findIndex(b => b.Name == settings.behavior) + } +}; + function init(settings) { // Remember the player ID that we change the AI settings for g_PlayerSlot = settings.playerSlot; - let aiSelection = Engine.GetGUIObjectByName("aiSelection"); - aiSelection.list = g_AIDescriptions.map(ai => ai.data.name); - aiSelection.selected = g_AIDescriptions.findIndex(ai => ai.id == settings.id); - aiSelection.hidden = !settings.isController; - - let aiSelectionText = Engine.GetGUIObjectByName("aiSelectionText"); - aiSelectionText.caption = aiSelection.list[aiSelection.selected]; - aiSelectionText.hidden = settings.isController; - - let aiDiff = Engine.GetGUIObjectByName("aiDifficulty"); - aiDiff.list = prepareForDropdown(g_Settings.AIDifficulties).Title; - aiDiff.selected = settings.difficulty; - aiDiff.hidden = !settings.isController; - - let aiDiffText = Engine.GetGUIObjectByName("aiDifficultyText"); - aiDiffText.caption = aiDiff.list[aiDiff.selected]; - aiDiffText.hidden = settings.isController; + for (let name in g_AIControls) + { + let control = Engine.GetGUIObjectByName(name); + control.list = g_AIControls[name].labels; + control.selected = g_AIControls[name].selected(settings); + control.hidden = !settings.isController; + + let label = Engine.GetGUIObjectByName(name + "Text"); + label.caption = control.list[control.selected]; + label.hidden = settings.isController; + } + + checkBehavior(); } function selectAI(idx) @@ -37,6 +48,19 @@ Engine.GetGUIObjectByName("aiDescription").caption = g_AIDescriptions[idx].data.description; } +/** Behavior choice does not apply for Sandbox level */ +function checkBehavior() +{ + if (g_Settings.AIDifficulties[Engine.GetGUIObjectByName("aiDifficulty").selected].Name != "sandbox") + { + Engine.GetGUIObjectByName("aiBehavior").enabled = true; + return; + } + let aiBehavior = Engine.GetGUIObjectByName("aiBehavior"); + aiBehavior.enabled = false; + aiBehavior.selected = g_Settings.AIBehaviors.findIndex(b => b.Name == "balanced"); +} + function returnAI(save = true) { let idx = Engine.GetGUIObjectByName("aiSelection").selected; @@ -48,6 +72,7 @@ "id": g_AIDescriptions[idx].id, "name": g_AIDescriptions[idx].data.name, "difficulty": Engine.GetGUIObjectByName("aiDifficulty").selected, + "behavior": g_Settings.AIBehaviors[Engine.GetGUIObjectByName("aiBehavior").selected].Name, "playerSlot": g_PlayerSlot }); } Index: binaries/data/mods/public/gui/aiconfig/aiconfig.xml =================================================================== --- binaries/data/mods/public/gui/aiconfig/aiconfig.xml +++ binaries/data/mods/public/gui/aiconfig/aiconfig.xml @@ -8,7 +8,7 @@ - + AI Configuration @@ -29,11 +29,20 @@ + checkBehavior(this.selected); + + + AI Behavior: + + + + + - + Cancel Index: binaries/data/mods/public/gui/common/gamedescription.js =================================================================== --- binaries/data/mods/public/gui/common/gamedescription.js +++ binaries/data/mods/public/gui/common/gamedescription.js @@ -102,19 +102,19 @@ { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)"); + playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIbehavior)s %(AIname)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, %(state)s)"); + playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIbehavior)s %(AIname)s, %(state)s)"); } else { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s)"); + playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIbehavior)s %(AIname)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s, %(state)s)"); + playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIbehavior)s %(AIname)s, %(state)s)"); } } else @@ -174,7 +174,8 @@ translateWithContext("playerstate", "won"), "AIname": isAI ? translateAIName(playerData.AI) : "", - "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : "" + "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : "", + "AIbehavior": isAI ? translateAIBehavior(playerData.AIBehavior) : "" })); } Index: binaries/data/mods/public/gui/common/settings.js =================================================================== --- binaries/data/mods/public/gui/common/settings.js +++ binaries/data/mods/public/gui/common/settings.js @@ -37,6 +37,7 @@ var settings = { "AIDescriptions": loadAIDescriptions(), "AIDifficulties": loadAIDifficulties(), + "AIBehaviors": loadAIBehaviors(), "Ceasefire": loadCeasefire(), "VictoryDurations": loadVictoryDuration(), "GameSpeeds": loadSettingValuesFile("game_speeds.json"), @@ -138,6 +139,29 @@ ]; } +function loadAIBehaviors() +{ + return [ + { + "Name": "random", + "Title": translateWithContext("aiBehavior", "Random"), + "Default": true + }, + { + "Name": "balanced", + "Title": translateWithContext("aiBehavior", "Balanced"), + }, + { + "Name": "defensive", + "Title": translateWithContext("aiBehavior", "Defensive") + }, + { + "Name": "aggressive", + "Title": translateWithContext("aiBehavior", "Aggressive") + } + ]; +} + /** * Loads available victory times for victory conditions like Wonder and Capture the Relic. */ @@ -359,6 +383,17 @@ /** * Returns title or placeholder. * + * @param {string} aiBehavior - for example "defensive" + */ +function translateAIBehavior(aiBehavior) +{ + let behavior = g_Settings.AIBehaviors.find(b => b.Name == aiBehavior); + return behavior ? behavior.Title : translateWithContext("AI behavior", "Default"); +} + +/** + * Returns title or placeholder. + * * @param {string} mapType - for example "skirmish" * @returns {string} */ Index: binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -955,9 +955,10 @@ "onPress": (playerIdx) => function() { openAIConfig(playerIdx); }, - "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(name)s - %(difficulty)s."), { + "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(difficulty)s %(behavior)s %(name)s."), { "name": translateAIName(g_GameAttributes.settings.PlayerData[playerIdx].AI), - "difficulty": translateAIDifficulty(g_GameAttributes.settings.PlayerData[playerIdx].AIDiff) + "difficulty": translateAIDifficulty(g_GameAttributes.settings.PlayerData[playerIdx].AIDiff), + "behavior": translateAIBehavior(g_GameAttributes.settings.PlayerData[playerIdx].AIBehavior), }), }, }; @@ -1016,6 +1017,7 @@ g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1)); let aiDifficulty = +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"); + let aiBehavior = Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"); // Don't change the underlying defaults file, as Atlas uses that file too for (let i in g_DefaultPlayerData) @@ -1023,6 +1025,7 @@ g_DefaultPlayerData[i].Civ = "random"; g_DefaultPlayerData[i].Team = -1; g_DefaultPlayerData[i].AIDiff = aiDifficulty; + g_DefaultPlayerData[i].AIBehavior = aiBehavior; } deepfreeze(g_DefaultPlayerData); @@ -2105,7 +2108,8 @@ "isController": g_IsController, "playerSlot": playerSlot, "id": g_GameAttributes.settings.PlayerData[playerSlot].AI, - "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff + "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff, + "behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior }); } @@ -2121,6 +2125,7 @@ g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id; g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty; + g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior; updateGameAttributes(); } @@ -2180,6 +2185,7 @@ // Transfer the AI from the target slot to the current slot. g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI; g_GameAttributes.settings.PlayerData[playerID - 1].AIDiff = g_GameAttributes.settings.PlayerData[newSlot].AIDiff; + g_GameAttributes.settings.PlayerData[playerID - 1].AIBehavior = g_GameAttributes.settings.PlayerData[newSlot].AIBehavior; // Swap civilizations and colors if they aren't fixed if (g_GameAttributes.mapType != "scenario") Index: binaries/data/mods/public/gui/options/options.json =================================================================== --- binaries/data/mods/public/gui/options/options.json +++ binaries/data/mods/public/gui/options/options.json @@ -369,7 +369,7 @@ }, { "type": "dropdown", - "label": "Default AI difficulty", + "label": "Default AI Difficulty", "tooltip": "Default difficulty of the AI.", "config": "gui.gamesetup.aidifficulty", "list": [ @@ -383,6 +383,18 @@ }, { "type": "dropdown", + "label": "Default AI Behavior", + "tooltip": "Default behavior of the AI.", + "config": "gui.gamesetup.aibehavior", + "list": [ + { "value": "random", "label": "Random" }, + { "value": "balanced", "label": "Balanced" }, + { "value": "aggressive", "label": "Aggressive" }, + { "value": "defensive", "label": "Defensive" } + ] + }, + { + "type": "dropdown", "label": "Assign Players", "tooltip": "Automatically assign joining clients to free player slots during the match setup.", "config": "gui.gamesetup.assignplayers", Index: binaries/data/mods/public/simulation/ai/petra/_petrabot.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/_petrabot.js +++ binaries/data/mods/public/simulation/ai/petra/_petrabot.js @@ -17,7 +17,7 @@ "transports": 1 // transport plans start at 1 because 0 might be used as none }; - this.Config = new m.Config(settings.difficulty); + this.Config = new m.Config(settings.difficulty, settings.behavior); this.savedEvents = {}; }; Index: binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/attackManager.js +++ binaries/data/mods/public/simulation/ai/petra/attackManager.js @@ -30,17 +30,17 @@ m.AttackManager.prototype.setRushes = function(allowed) { - if (this.Config.personality.aggressive > 0.8 && allowed > 2) + if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) { this.maxRushes = 3; this.rushSize = [ 16, 20, 24 ]; } - else if (this.Config.personality.aggressive > 0.6 && allowed > 1) + else if (this.Config.personality.aggressive > this.Config.personalityCut.medium && allowed > 1) { this.maxRushes = 2; this.rushSize = [ 18, 22 ]; } - else if (this.Config.personality.aggressive > 0.3 && allowed > 0) + else if (this.Config.personality.aggressive > this.Config.personalityCut.weak && allowed > 0) { this.maxRushes = 1; this.rushSize = [ 20 ]; Index: binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/config.js +++ binaries/data/mods/public/simulation/ai/petra/config.js @@ -1,11 +1,14 @@ var PETRA = function(m) { -m.Config = function(difficulty) +m.Config = function(difficulty, behavior) { // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. this.difficulty = difficulty !== undefined ? difficulty : 3; + // for instance "balanced", "aggressive" or "defensive" + this.behavior = behavior || "random"; + // debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug this.debug = 0; @@ -88,12 +91,28 @@ "emergency": 1000 // used only in emergency situations, should be the highest one }; + // Default personality (will be updated in setConfig) this.personality = { "aggressive": 0.5, "cooperative": 0.5, "defensive": 0.5 }; + // The parameter used to define the personality is basically the aggressivity or (1-defensiveness) + // as they are anticorrelated, although some small smearing to decorelate them will be added. + this.personalityList = { + "random": { "min": 0, "max": 1 }, + "defensive": { "min": 0, "max": 0.27 }, + "balanced": { "min": 0.37, "max": 0.63 }, + "aggressive": { "min": 0.73, "max": 1 } + }, + // Petra usually uses the continuous values of personality.aggressive and personality.defensive + // to define its behavior according to personality. But when discontinuous behavior is needed, + // it uses the following personalityCut which should be set such that: + // behavior="aggressive" => personality.aggressive > personalityCut.strong && + // personality.defensive < personalityCut.weak + // and inversely for behavior="defensive" + this.personalityCut = { "weak": 0.3, "medium": 0.5, "strong": 0.7 }; // See m.QueueManager.prototype.wantedGatherRates() this.queues = @@ -121,18 +140,23 @@ m.Config.prototype.setConfig = function(gameState) { - // initialize personality traits - if (this.difficulty > 1) + if (this.difficulty > 0) { - this.personality.aggressive = randFloat(0, 1); - this.personality.cooperative = randFloat(0, 1); - this.personality.defensive = randFloat(0, 1); - } - else - { - this.personality.aggressive = 0.1; - this.personality.cooperative = 0.9; + // Setup personality traits + let behavior = randFloat(-0.5, 0.5); + // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety + let variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); + let aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; + let defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; + let min = this.personalityList[this.behavior].min; + let max = this.personalityList[this.behavior].max; + this.personality = { + "aggressive": min + aggressive * (max - min), + "defensive": 1 - max + defensive * (max - min), + "cooperative": randFloat(0, 1) + }; } + if (gameState.playerData.teamsLocked) this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30); else if (gameState.getAlliedVictory()) Index: binaries/data/mods/public/simulation/data/settings/player_defaults.json =================================================================== --- binaries/data/mods/public/simulation/data/settings/player_defaults.json +++ binaries/data/mods/public/simulation/data/settings/player_defaults.json @@ -6,63 +6,72 @@ "Civ": "gaia", "Color": { "r": 255, "g": 255, "b": 255 }, "AI": "", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 1", "Civ": "athen", "Color": { "r": 21, "g": 55, "b": 149 }, "AI": "", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 2", "Civ": "cart", "Color": { "r": 150, "g": 20, "b": 20 }, "AI": "petra", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 3", "Civ": "gaul", "Color": { "r": 86 , "g": 180, "b": 31 }, "AI": "petra", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 4", "Civ": "iber", "Color": { "r": 231, "g": 200, "b": 5 }, "AI": "petra", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 5", "Civ": "mace", "Color": { "r": 50, "g": 170, "b": 170 }, "AI": "petra", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 6", "Civ": "maur", "Color": { "r": 160, "g": 80, "b": 200 }, "AI": "petra", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 7", "Civ": "pers", "Color": { "r": 220, "g": 115, "b": 16 }, "AI": "petra", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" }, { "Name": "Player 8", "Civ": "rome", "Color": { "r": 64, "g": 64, "b": 64 }, "AI": "petra", - "AIDiff": 3 + "AIDiff": 3, + "AIBehavior": "random" } ] } Index: binaries/data/mods/public/simulation/helpers/InitGame.js =================================================================== --- binaries/data/mods/public/simulation/helpers/InitGame.js +++ binaries/data/mods/public/simulation/helpers/InitGame.js @@ -51,7 +51,7 @@ if (settings.PlayerData[i] && settings.PlayerData[i].AI && settings.PlayerData[i].AI != "") { let AIDiff = +settings.PlayerData[i].AIDiff; - cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, AIDiff); + cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, AIDiff, settings.PlayerData[i].AIBehavior || "random"); cmpPlayer.SetAI(true); AIDiff = Math.min(AIDiff, rate.length - 1); cmpPlayer.SetGatherRateMultiplier(rate[AIDiff]); Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -1367,6 +1367,7 @@ CStr name = aiArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player, "AI", std::string(name)); scriptInterface.SetProperty(player, "AIDiff", 3); + scriptInterface.SetProperty(player, "AIBehavior", std::string("balanced")); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } Index: source/simulation2/components/CCmpAIManager.cpp =================================================================== --- source/simulation2/components/CCmpAIManager.cpp +++ source/simulation2/components/CCmpAIManager.cpp @@ -82,9 +82,9 @@ { NONCOPYABLE(CAIPlayer); public: - CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, + CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior, shared_ptr scriptInterface) : - m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), + m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetJSRuntime()) { } @@ -148,6 +148,8 @@ m_ScriptInterface->Eval(L"({})", &settings); m_ScriptInterface->SetProperty(settings, "player", m_Player, false); m_ScriptInterface->SetProperty(settings, "difficulty", m_Difficulty, false); + m_ScriptInterface->SetProperty(settings, "behavior", m_Behavior, false); + if (!m_UseSharedComponent) { ENSURE(m_Worker.m_HasLoadedEntityTemplates); @@ -188,6 +190,7 @@ std::wstring m_AIName; player_id_t m_Player; u8 m_Difficulty; + std::wstring m_Behavior; bool m_UseSharedComponent; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted @@ -477,9 +480,9 @@ return true; } - bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty) + bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior) { - shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, m_ScriptInterface)); + shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, behavior, m_ScriptInterface)); if (!ai->Initialise()) return false; @@ -495,7 +498,7 @@ bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { - // this will be run last by InitGame.Js, passing the full game representation. + // this will be run last by InitGame.js, passing the full game representation. // For now it will run for the shared Component. // This is NOT run during deserialization. JSContext* cx = m_ScriptInterface->GetContext(); @@ -692,6 +695,7 @@ serializer.String("name", m_Players[i]->m_AIName, 1, 256); serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty); + serializer.String("behavior", m_Players[i]->m_Behavior, 1, 256); serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size()); for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) @@ -763,10 +767,12 @@ std::wstring name; player_id_t player; u8 difficulty; + std::wstring behavior; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); - if (!AddPlayer(name, player, difficulty)) + deserializer.String("behavior", behavior, 1, 256); + if (!AddPlayer(name, player, difficulty, behavior)) throw PSERROR_Deserialize_ScriptError(); u32 numCommands; @@ -999,11 +1005,11 @@ m_JustDeserialized = true; } - virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty) + virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior) { LoadUsedEntityTemplates(); - m_Worker.AddPlayer(id, player, difficulty); + m_Worker.AddPlayer(id, player, difficulty, behavior); // AI players can cheat and see through FoW/SoD, since that greatly simplifies // their implementation. Index: source/simulation2/components/ICmpAIManager.h =================================================================== --- source/simulation2/components/ICmpAIManager.h +++ source/simulation2/components/ICmpAIManager.h @@ -30,7 +30,7 @@ * by @p id (corresponding to a subdirectory in simulation/ai/), * to control player @p player. */ - virtual void AddPlayer(const std::wstring& id, player_id_t player, uint8_t difficulty) = 0; + virtual void AddPlayer(const std::wstring& id, player_id_t player, uint8_t difficulty, const std::wstring&) = 0; virtual void SetRNGSeed(uint32_t seed) = 0; virtual void TryLoadSharedComponent() = 0; virtual void RunGamestateInit() = 0; Index: source/simulation2/components/ICmpAIManager.cpp =================================================================== --- source/simulation2/components/ICmpAIManager.cpp +++ source/simulation2/components/ICmpAIManager.cpp @@ -25,7 +25,7 @@ #include "ps/Filesystem.h" BEGIN_INTERFACE_WRAPPER(AIManager) -DEFINE_INTERFACE_METHOD_3("AddPlayer", void, ICmpAIManager, AddPlayer, std::wstring, player_id_t, uint8_t) +DEFINE_INTERFACE_METHOD_4("AddPlayer", void, ICmpAIManager, AddPlayer, std::wstring, player_id_t, uint8_t, std::wstring) DEFINE_INTERFACE_METHOD_1("SetRNGSeed", void, ICmpAIManager, SetRNGSeed, uint32_t) DEFINE_INTERFACE_METHOD_0("TryLoadSharedComponent", void, ICmpAIManager, TryLoadSharedComponent) DEFINE_INTERFACE_METHOD_0("RunGamestateInit", void, ICmpAIManager, RunGamestateInit)