#!/usr/bin/env python3 """Install the version-checked Classic adapter into an isolated CMaNGOS checkout.""" import argparse import pathlib import shutil def patch(path, anchor, replacement): text = path.read_text() if replacement in text: return if text.count(anchor) != 1: raise RuntimeError(f"Upstream source changed: {path.name}") path.write_text(text.replace(anchor, replacement)) def install(source): here = pathlib.Path(__file__).resolve().parent for path in (here / "native").glob("AzerothAI.*"): shutil.copy2(path, source / "src/game/World" / path.name) for relative, anchor, invocation in [ ("src/game/World/World.cpp", " sMapMgr.Update(diff);", "AzerothAIUpdate(diff);"), ("src/game/Entities/Unit.cpp", " uint32 health = victim->GetHealth();", "damage = AzerothAIBoundDamage(victim, damage);") ]: path = source / relative include = '#ifdef ENABLE_PLAYERBOTS\n#include "World/AzerothAI.h"\n#endif\n' if include not in path.read_text(): path.write_text(include + path.read_text()) patch(path, anchor, anchor + "\n#ifdef ENABLE_PLAYERBOTS\n " + invocation + "\n#endif") # Upstream treats unknown gear modes as generated equipment. Explicitly # preserve ordinary character-creation equipment for our level-one roster. path = source / "src/modules/PlayerBots/playerbot/PlayerbotMgr.cpp" anchor = ' sRandomPlayerbotMgr.SetValue(botGuid, "create gear", 1, gear);' patch(path, anchor, ' if (gear != "none")\n ' + anchor) path = source / "src/game/Entities/CharacterHandler.cpp" include = '#ifdef ENABLE_PLAYERBOTS\n#include "World/AzerothAI.h"\n#endif\n' if include not in path.read_text(): path.write_text(include + path.read_text()) anchor = 'void PlayerbotHolder::AddPlayerBot(uint32 playerGuid, uint32 masterAccount)\n{' patch(path, anchor, anchor + '\n if (!AzerothAIAllowBotLogin(playerGuid)) return;') # Hooks record successful native outcomes, not inferred command success. player = source / "src/game/Entities/Player.cpp" loot = source / "src/game/Loot/LootMgr.cpp" for path in [player, loot]: if include not in path.read_text(): path.write_text(include + path.read_text()) hooks = [ (player, " // resend quests status directly\n UpdateForQuestWorldObjects();", 'AzerothAIEvent(this,"player.quest.reward",quest_id);'), (player, "void Player::CompleteQuest(uint32 quest_id)\n{\n if (quest_id)\n {\n SetQuestStatus(quest_id, QUEST_STATUS_COMPLETE);", 'AzerothAIEvent(this,"player.quest.complete",quest_id);'), (player, " SetDeathState(CORPSE);", 'AzerothAIEvent(this,"player.death");'), (loot, " plr->ModifyMoney(money_per_player);", 'AzerothAIEvent(plr,"player.loot.money",0,money_per_player);'), (loot, " player->ModifyMoney(m_gold);", 'AzerothAIEvent(player,"player.loot.money",0,m_gold);'), ] for path, anchor, call in hooks: if call: patch(path, anchor, anchor + "\n#ifdef ENABLE_PLAYERBOTS\n " + call + "\n#endif") for anchor, call in [ (" if (!applySickness)\n return;", 'AzerothAIEvent(this,"player.resurrect");'), (" WorldPacket data(SMSG_LEARNED_SPELL, 4);", 'AzerothAIEvent(this,"player.spell.learn",spell_id);'), (" WorldPacket data(SMSG_ITEM_PUSH_RESULT, (8 + 4 + 4 + 4 + 1 + 4 + 4 + 4 + 4 + 4));", 'if (!received && !created) AzerothAIEvent(this,"player.loot.item",item->GetEntry(),count);'), ]: patch(player, anchor, "#ifdef ENABLE_PLAYERBOTS\n " + call + "\n#endif\n" + anchor) chat=source / "src/game/Chat/ChatHandler.cpp" if include not in chat.read_text():chat.write_text(include+chat.read_text()) for anchor,call in [ (' GetPlayer()->Say(msg, lang);', 'if(lang!=LANG_ADDON && !m_anticheat->IsSilenced()) AzerothAIChat(GetPlayer(),msg);'), (' if (lang != LANG_ADDON && !m_anticheat->IsSilenced())\n m_anticheat->Whisper(msg, player->GetObjectGuid());', 'if(lang!=LANG_ADDON && !m_anticheat->IsSilenced()) AzerothAIChat(GetPlayer(),msg,player);') ]:patch(chat,anchor,anchor+'\n#ifdef ENABLE_PLAYERBOTS\n '+call+'\n#endif') if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("source", type=pathlib.Path) install(parser.parse_args().source.resolve())