From b7ef71c29e4c5b917696f260f9c34bb891033a80 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:00:57 -0600 Subject: [PATCH] Major gameobject refactor, Tile/Nav unloading and others. (#1437) * Closes #1201 Closes #1201 * Closes #571 Closes #571 * Update updates.sql * Closes #699 Closes #699 * Update updates.sql * Update UnitManager.py * Update GameObjectLootManager.py * Update FearMovement.py * Update FearMovement.py * Update FearMovement.py * Crushing blow. * Update UnitManager.py * Update AuraManager.py * Specialization.. * Update ExtendedSpellData.py * Update updates.sql * Update QuestManager.py * Update updates.sql * Fix quest status display bug. - Unavailable quest due race / class requirements / disabled should no longer wrongfully display available in the future (gray symbol) * Update QuestManager.py * Update updates.sql * Update updates.sql * Update QuestHelpers.py err * Update ReadItemHandler.py * CMSG_READ_ITEM * Modify both UnitFlag and UnitState when possible. - Fix ConfusedMovement not applying unit flag. * Update UnitManager.py * Update updates.sql * Route all flag changes through setters. * Update LootManager.py * Update PlayerManager.py * Remove gold by surrounding units gold mean. * Remove second pass for loot generation. Left comment. * Update updates.sql * Update PlayerManager.py * Update PlayerManager.py * Update GameObjectLootManager.py * Update UnitManager.py * Update updates.sql * Major gameobject refactor. - All gameobject types are now implemented via inheritance. - Implement Doors/Buttons auto reset. - Fix mining nodes despawn chance calculation. - Fishing node activation (Bobber animation) is now notified instantly to clients reducing the missed hook errors. - Fix despawn bug that did not destroy the object to near players when only the is_spanwed flag changed to False. - Add GM collision cheat. '.collision' * Update GameObjectBuilder.py * Update PlayerManager.py * Update ItemCodes.py * Update SpellCodes.py * Update SpellCodes.py * Update ChestMananger.py * Make sure parent GameObjectManager update() is called. * Update ButtonManager.py * Update DoorManager.py * Minor * Update MapManager.py * Update AttackSwingHandler.py * Update AttackSwingHandler.py * Implement Tile unloading. - Deactivated cells will now also unload tile data (.map) and namigator data (.nav) when possible. - Many optimizations in order to also load less data when activating cells. (Only necessary tiles and nav data). - Fixed a bug in which creatures could also trigger tile/nav loading (This is restricted to players, creatures can activate cells but should not load data) - Fixes some issues with namigator and wmo based maps which left this kind of maps with no navigation. * Update MapTile.py * Update CommandManager.py * Improve graceful shutdown. * Update main.py * Stop inside PyCharm will now also achieve a graceful shutdown. * Update WorldManager.py * Update main.py * Update main.py * Update CreatureManager.py * #1438 * Fix bug with respawned objects not displaying to players. * Update updates.sql * Update updates.sql * Update DoorManager.py * Respawn. - Restore Gameobject original state each respawn() - Restore Creature original state each respawn() * Use creature addon equipment when available. - Add missing goldshire guard. * Proper lamp for new guard. * Update VirtualItemUtils.py * In the Name of the Light - Min Level 23 * In the Name of the Light Zone sort set to Southshore (given quest giver location), alpha does not have dungeon quest sort ids. #1438 complete. --- etc/config/config.yml.dist | 3 +- etc/databases/world/updates/updates.sql | 55 +++ game/realm/RealmManager.py | 76 ++-- game/world/WorldManager.py | 161 ++++----- game/world/managers/CommandManager.py | 43 ++- game/world/managers/maps/Cell.py | 12 + game/world/managers/maps/GridManager.py | 74 ++-- game/world/managers/maps/Map.py | 19 +- game/world/managers/maps/MapManager.py | 215 ++++++------ game/world/managers/maps/MapTile.py | 47 ++- game/world/managers/maps/helpers/MapUtils.py | 37 ++ game/world/managers/maps/helpers/Namigator.py | 5 +- game/world/managers/objects/ObjectManager.py | 7 +- .../managers/objects/corpse/CorpseManager.py | 6 + .../objects/dynamic/DynamicObjectManager.py | 6 + .../objects/gameobjects/GameObjectBuilder.py | 109 ++++-- .../objects/gameobjects/GameObjectManager.py | 326 +++++------------- .../objects/gameobjects/GameObjectSpawn.py | 4 +- .../gameobjects/managers/ButtonManager.py | 80 ++++- .../gameobjects/managers/CameraManager.py | 34 ++ .../gameobjects/managers/ChairManager.py | 43 +++ .../gameobjects/managers/ChestMananger.py | 74 ++-- .../gameobjects/managers/DoorManager.py | 77 +++++ .../managers/FishingNodeManager.py | 74 ++-- .../gameobjects/managers/GooberManager.py | 97 +++++- .../gameobjects/managers/MiningNodeManager.py | 64 +++- .../gameobjects/managers/QuestGiverManager.py | 19 + .../gameobjects/managers/RitualManager.py | 113 +++--- .../gameobjects/managers/SpellFocusManager.py | 30 +- .../gameobjects/managers/TransportManager.py | 123 ++++--- .../gameobjects/managers/TrapManager.py | 110 ++++-- .../managers/objects/item/ItemManager.py | 8 + game/world/managers/objects/script/Script.py | 8 +- .../managers/objects/script/ScriptHandler.py | 10 +- .../managers/objects/spell/EffectTargets.py | 6 +- .../objects/spell/SpellEffectHandler.py | 24 +- .../managers/objects/spell/SpellManager.py | 30 +- .../objects/units/creature/CreatureManager.py | 21 +- .../units/creature/items/VirtualItemUtils.py | 15 +- .../objects/units/movement/MovementInfo.py | 2 +- .../movement/behaviors/WaypointMovement.py | 4 +- .../objects/units/player/PlayerManager.py | 28 +- main.py | 125 +++---- network/packet/update/UpdateManager.py | 8 +- utils/ConfigManager.py | 2 +- utils/constants/MiscCodes.py | 6 + utils/constants/SpellCodes.py | 26 +- 47 files changed, 1533 insertions(+), 933 deletions(-) create mode 100644 game/world/managers/maps/helpers/MapUtils.py create mode 100644 game/world/managers/objects/gameobjects/managers/CameraManager.py create mode 100644 game/world/managers/objects/gameobjects/managers/ChairManager.py create mode 100644 game/world/managers/objects/gameobjects/managers/DoorManager.py create mode 100644 game/world/managers/objects/gameobjects/managers/QuestGiverManager.py diff --git a/etc/config/config.yml.dist b/etc/config/config.yml.dist index 2663606e6..3c06693a6 100644 --- a/etc/config/config.yml.dist +++ b/etc/config/config.yml.dist @@ -1,5 +1,5 @@ Version: - current: 17 + current: 18 Database: Connection: @@ -31,6 +31,7 @@ Server: xp_rate: 1.0 load_gameobjects: True load_creatures: True + load_pools: True # If False, creatures and gameobject spawns will ignore pooling. supported_client: 3368 realm_saving_interval_seconds: 60 cell_size: 64 # Shouldn't be much bigger than 200 diff --git a/etc/databases/world/updates/updates.sql b/etc/databases/world/updates/updates.sql index 61697ed9d..1d2ef1b17 100644 --- a/etc/databases/world/updates/updates.sql +++ b/etc/databases/world/updates/updates.sql @@ -188,6 +188,61 @@ begin not atomic -- Chest placement. UPDATE `spawns_gameobjects` SET `spawn_positionX` = '10720.001', `spawn_positionY` = '758.654', `spawn_positionZ` = '1322.234' WHERE (`spawn_id` = '49528'); + -- Down the Scarlet Path (ID 261) https://github.com/The-Alpha-Project/alpha-core/issues/1438 + UPDATE `quest_template` SET `ZoneOrSort` = '10', `MinLevel` = '23', `QuestLevel` = '28', `Details` = 'I will be frank. We are at war with the Scourge. It is an evil that corrupts our people and infects our land. It must be stopped before it washes over our last bastions and drags our world into shadow. We of the Scarlet Crusade have sworn to fight the Scourge with body and soul.$B$BIf you would take this same oath, then gather your courage and prove your allegiance - wage war with the Undead of Duskwood, and return to me with proof of your deeds. $B$BDo this, and the Crusade will embrace you.', `Objectives` = 'Bring 12 Shriveled Eyes to Brother Anton in Stormwind.', `ReqCreatureOrGOId1` = '2477', `ReqCreatureOrGOCount1` = '12', `RewXP` = '2050', `RewOrReqMoney` = '2000' WHERE (`entry` = '261'); + + -- Down the Scarlet Path (ID 1052) https://github.com/The-Alpha-Project/alpha-core/issues/1438 + UPDATE `quest_template` SET `ZoneOrSort` = '10', `MinLevel` = '23', `QuestLevel` = '28', `Details` = 'We of the Scarlet Crusade lay claim to strongholds from Hearthglen to Tirisfal Glades. We are quite proud of our bastions of cleansing throughout Lordaeron.$b$bYou have proven yourself against the undead in southern Azeroth. But the true threat of the plague lies in the northern lands of Lordaeron.$b$bTravel to the town of Southshore, in the Eastern Kingdoms. Seek out a crusader named Raleigh the Devout. Give him this letter of commendation bearing my seal and he will escort you to a place of honor in our Scarlet Monastery.', `RewXP` = '1200' WHERE (`entry` = '1052'); + + -- In the Name of the Light (ID 1053) https://github.com/The-Alpha-Project/alpha-core/issues/1438 + UPDATE `quest_template` SET `ZoneOrSort` = '271', `MinLevel` = '23', `QuestLevel` = '35', `RewItemId1` = '1217', `RewItemCount1` = '1', `RewXP` = '8650' WHERE (`entry` = '1053'); + + -- Brother Anton, no equipment and level. + UPDATE `creature_template` SET `level_min` = '50', `level_max` = '50', `equipment_id` = '0' WHERE (`entry` = '1182'); + + -- Fix Raleigh the Devout Hammer, remove offhand non existent book. + UPDATE `creature_equip_template` SET `equipentry1` = '2524', `equipentry2` = '0' WHERE (`entry` = '3980'); + + -- Ravager's Skull (ID 2477) should be renamed to "Shriveled Eye" + UPDATE `item_template` SET `name` = 'Shriveled Eye' WHERE (`entry` = '2477'); + + -- Ravager's Skull (ID 2477) drop. Brain Eaters, Plague Spreaders, Bone Chewers, Fetid Corpses and Rotted Ones. + DELETE FROM `creature_loot_template` WHERE (`item` = '2477'); + INSERT INTO `creature_loot_template` (`entry`, `item`, `ChanceOrQuestChance`, `groupid`, `mincountOrRef`, `maxcount`, `condition_id`) VALUES ('570', '2477', '-80', '0', '1', '1', '0'); + INSERT INTO `creature_loot_template` (`entry`, `item`, `ChanceOrQuestChance`, `groupid`, `mincountOrRef`, `maxcount`, `condition_id`) VALUES ('604', '2477', '-80', '0', '1', '1', '0'); + INSERT INTO `creature_loot_template` (`entry`, `item`, `ChanceOrQuestChance`, `groupid`, `mincountOrRef`, `maxcount`, `condition_id`) VALUES ('210', '2477', '-40', '0', '1', '1', '0'); + INSERT INTO `creature_loot_template` (`entry`, `item`, `ChanceOrQuestChance`, `groupid`, `mincountOrRef`, `maxcount`, `condition_id`) VALUES ('127', '2477', '-15', '0', '1', '1', '0'); + INSERT INTO `creature_loot_template` (`entry`, `item`, `ChanceOrQuestChance`, `groupid`, `mincountOrRef`, `maxcount`, `condition_id`) VALUES ('948', '2477', '-15', '0', '1', '1', '0'); + + -- Fix Raleigh the Devout timing for Respawn Gobject, should appear right when 'Raleigh the Devout throws Anton's letter down on the table.' happens. + DELETE FROM `quest_end_scripts` WHERE `id`=1052; + INSERT INTO `quest_end_scripts` (`id`, `delay`, `priority`, `command`, `datalong`, `datalong2`, `datalong3`, `datalong4`, `target_param1`, `target_param2`, `target_type`, `data_flags`, `dataint`, `dataint2`, `dataint3`, `dataint4`, `x`, `y`, `z`, `o`, `condition_id`, `comments`) VALUES + (1052, 0, 0, 1, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Emote'), + (1052, 0, 0, 4, 147, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Modify Flags'), + (1052, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1377, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Talk'), + (1052, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1378, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Talk'), + (1052, 7, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Emote'), + (1052, 8, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -848.237, -577.427, 18.546, 0, 0, 'Raleigh the Devout - Move'), + (1052, 14, 0, 1, 61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Emote'), + (1052, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1379, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Talk'), + (1052, 15, 0, 9, 133, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Respawn Gobject'), + (1052, 20, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 1906, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Set Equipment'), + (1052, 23, 0, 1, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Emote'), + (1052, 24, 0, 13, 0, 0, 0, 0, 133, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Activate Gobject'), + (1052, 26, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -844.878, -580.284, 18.5459, 2.391, 0, 'Raleigh the Devout - Move'), + (1052, 28, 0, 19, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Set Equipment'), + (1052, 31, 0, 4, 147, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Raleigh the Devout - Modify Flags'); + + -- Anton's Letter of Commendation + UPDATE `gameobject_template` SET `flags` = '4' WHERE (`entry` = '19534'); + + -- Missing Goldshire guard. + INSERT INTO `spawns_creatures` (`spawn_id`, `spawn_entry1`, `spawn_entry2`, `spawn_entry3`, `spawn_entry4`, `map`, `position_x`, `position_y`, `position_z`, `orientation`, `spawntimesecsmin`, `spawntimesecsmax`, `wander_distance`, `health_percent`, `mana_percent`, `movement_type`, `spawn_flags`, `visibility_mod`, `ignored`) VALUES ('400464', '1423', '0', '0', '0', '0', '-9497.842', '67.635', '56.367', '6.131', '300', '300', '0', '100', '100', '0', '0', '0', '0'); + + -- Addon - Equipment id. + INSERT INTO `creature_addon` (`guid`, `display_id`, `mount_display_id`, `equipment_id`, `stand_state`, `sheath_state`, `emote_state`) VALUES ('400464', '0', '0', '400464', '0', '1', '0'); + INSERT INTO `creature_equip_template` (`entry`, `equipentry1`, `equipentry2`, `equipentry3`) VALUES ('400464', '2714', '143', '1899'); + insert into applied_updates values ('040920242'); end if; end $ diff --git a/game/realm/RealmManager.py b/game/realm/RealmManager.py index e75362426..dda6edbf7 100644 --- a/game/realm/RealmManager.py +++ b/game/realm/RealmManager.py @@ -70,49 +70,49 @@ def redirect_to_world(sck): sck.sendall(packet) @staticmethod - def start_realm(): + def start_realm(running): local_realm = REALMLIST[config.Server.Connection.Realm.local_realm_id] - server_socket = RealmManager.build_socket(local_realm.realm_address, local_realm.realm_port) - server_socket.listen() - real_binding = server_socket.getsockname() - # Make sure all characters have online = 0 on realm start. - RealmDatabaseManager.character_set_all_offline() - Logger.success(f'Login server started, listening on {real_binding[0]}:{real_binding[1]}\a') - - while True: - try: - client_socket, client_address = server_socket.accept() - RealmManager.serve_realmlist(client_socket) - client_socket.shutdown(socket.SHUT_RDWR) - client_socket.close() - except socket.timeout: - pass # Non blocking. - except OSError: - Logger.warning(traceback.format_exc()) - except KeyboardInterrupt: - break + with RealmManager.build_socket(local_realm.realm_address, local_realm.realm_port) as server_socket: + server_socket.listen() + real_binding = server_socket.getsockname() + # Make sure all characters have online = 0 on realm start. + RealmDatabaseManager.character_set_all_offline() + Logger.success(f'Login server started, listening on {real_binding[0]}:{real_binding[1]}\a') + + while running.value: + try: + client_socket, client_address = server_socket.accept() + RealmManager.serve_realmlist(client_socket) + client_socket.shutdown(socket.SHUT_RDWR) + client_socket.close() + except socket.timeout: + pass # Non blocking. + except OSError: + Logger.warning(traceback.format_exc()) + except KeyboardInterrupt: + break Logger.info("Login server turned off.") @staticmethod - def start_proxy(): + def start_proxy(running): local_realm = REALMLIST[config.Server.Connection.Realm.local_realm_id] - server_socket = RealmManager.build_socket(local_realm.proxy_address, local_realm.proxy_port) - server_socket.listen() - real_binding = server_socket.getsockname() - Logger.success(f'Proxy server started, listening on {real_binding[0]}:{real_binding[1]}\a') - - while True: - try: - client_socket, client_address = server_socket.accept() - RealmManager.redirect_to_world(client_socket) - client_socket.shutdown(socket.SHUT_RDWR) - client_socket.close() - except socket.timeout: - pass # Non blocking. - except OSError: - Logger.warning(traceback.format_exc()) - except KeyboardInterrupt: - break + with RealmManager.build_socket(local_realm.proxy_address, local_realm.proxy_port) as server_socket: + server_socket.listen() + real_binding = server_socket.getsockname() + Logger.success(f'Proxy server started, listening on {real_binding[0]}:{real_binding[1]}\a') + + while running.value: + try: + client_socket, client_address = server_socket.accept() + RealmManager.redirect_to_world(client_socket) + client_socket.shutdown(socket.SHUT_RDWR) + client_socket.close() + except socket.timeout: + pass # Non blocking. + except OSError: + Logger.warning(traceback.format_exc()) + except KeyboardInterrupt: + break Logger.info("Proxy server turned off.") diff --git a/game/world/WorldManager.py b/game/world/WorldManager.py index 87a410838..7e7450439 100644 --- a/game/world/WorldManager.py +++ b/game/world/WorldManager.py @@ -20,7 +20,6 @@ STARTUP_TIME = time() WORLD_ON = True - MAX_PACKET_BYTES = 4096 @@ -213,70 +212,7 @@ def receive_all(self, sck, expected_size): return buffer @staticmethod - def schedule_background_tasks(): - # Save characters. - realm_saving_scheduler = BackgroundScheduler() - realm_saving_scheduler._daemon = True - realm_saving_scheduler.add_job(WorldSessionStateHandler.save_characters, 'interval', - seconds=config.Server.Settings.realm_saving_interval_seconds, max_instances=1) - realm_saving_scheduler.start() - - # Player updates. - player_update_scheduler = BackgroundScheduler() - player_update_scheduler._daemon = True - player_update_scheduler.add_job(WorldSessionStateHandler.update_players, 'interval', seconds=0.1, - max_instances=1) - player_update_scheduler.start() - - # Creature updates. - creature_update_scheduler = BackgroundScheduler() - creature_update_scheduler._daemon = True - creature_update_scheduler.add_job(MapManager.update_creatures, 'interval', seconds=0.2, max_instances=1) - creature_update_scheduler.start() - - # Gameobject updates. - gameobject_update_scheduler = BackgroundScheduler() - gameobject_update_scheduler._daemon = True - gameobject_update_scheduler.add_job(MapManager.update_gameobjects, 'interval', seconds=1.0, max_instances=1) - gameobject_update_scheduler.start() - - # Dynamicobject updates. - dynobject_update_scheduler = BackgroundScheduler() - dynobject_update_scheduler._daemon = True - dynobject_update_scheduler.add_job(MapManager.update_dynobjects, 'interval', seconds=1.0, max_instances=1) - dynobject_update_scheduler.start() - - # Creature and Gameobject spawn updates (mostly to handle respawn logic). - spawn_update_scheduler = BackgroundScheduler() - spawn_update_scheduler._daemon = True - spawn_update_scheduler.add_job(MapManager.update_spawns, 'interval', seconds=1.0, max_instances=1) - spawn_update_scheduler.start() - - # Corpses updates. - corpses_update_scheduler = BackgroundScheduler() - corpses_update_scheduler._daemon = True - corpses_update_scheduler.add_job(MapManager.update_corpses, 'interval', seconds=10.0, max_instances=1) - corpses_update_scheduler.start() - - # Scripts/MapEvents events updates. - map_events_update_scheduler = BackgroundScheduler() - map_events_update_scheduler._daemon = True - map_events_update_scheduler.add_job(MapManager.update_map_scripts_and_events, 'interval', seconds=1.0, - max_instances=1) - map_events_update_scheduler.start() - - # MapManager tile loading. - tile_loading_scheduler = BackgroundScheduler() - tile_loading_scheduler._daemon = True - tile_loading_scheduler.add_job(MapManager.initialize_pending_tiles, 'interval', seconds=2.0, max_instances=4) - tile_loading_scheduler.start() - - # Cell deactivation. - cell_unloading_scheduler = BackgroundScheduler() - cell_unloading_scheduler._daemon = True - cell_unloading_scheduler.add_job(MapManager.deactivate_cells, 'interval', seconds=120.0, max_instances=1) - cell_unloading_scheduler.start() - + def start_chat_logger(): # Chat logging queue. if config.Server.Logging.log_player_chat: logging_thread = threading.Thread(target=ChatLogManager.process_logs) @@ -284,30 +220,81 @@ def schedule_background_tasks(): logging_thread.start() @staticmethod - def start(): - WorldLoader.load_data() + def build_get_schedulers(): + return [ + WorldServerSessionHandler.build_scheduler('Realm Saving', WorldSessionStateHandler.save_characters, + config.Server.Settings.realm_saving_interval_seconds, 1), + WorldServerSessionHandler.build_scheduler('Player', WorldSessionStateHandler.update_players, 0.1, 1), + WorldServerSessionHandler.build_scheduler('Creature', MapManager.update_creatures, 0.2, 1), + WorldServerSessionHandler.build_scheduler('Gameobject', MapManager.update_gameobjects, 1.0, 1), + WorldServerSessionHandler.build_scheduler('DynObject', MapManager.update_dynobjects, 1.0, 1), + WorldServerSessionHandler.build_scheduler('Spawn', MapManager.update_spawns, 1.0, 1), + WorldServerSessionHandler.build_scheduler('Corpse', MapManager.update_corpses, 10.0, 1), + WorldServerSessionHandler.build_scheduler('Script/Event', MapManager.update_map_scripts_and_events, 1.0, 1), + WorldServerSessionHandler.build_scheduler('Tile Loading', MapManager.initialize_pending_tiles, 2.0, 4), + WorldServerSessionHandler.build_scheduler('Tile Unloading', MapManager.deactivate_cells, 300.0, 1)] - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - # Use SO_REUSEADDR if SO_REUSEPORT doesn't exist. - except AttributeError: - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.bind((config.Server.Connection.WorldServer.host, config.Server.Connection.WorldServer.port)) - server_socket.listen() + @staticmethod + def build_scheduler(name, target, seconds, instances, daemon=True): + scheduler = BackgroundScheduler() + scheduler.daemon = daemon + scheduler.add_job(target, 'interval', seconds=seconds, max_instances=instances) + return scheduler + + @staticmethod + def start_schedulers(schedulers): + length = len(schedulers) + count = 0 - WorldServerSessionHandler.schedule_background_tasks() + for scheduler in schedulers: + scheduler.start() + count += 1 + Logger.progress('Loading background schedulers...', count, length) - real_binding = server_socket.getsockname() - Logger.success(f'World server started, listening on {real_binding[0]}:{real_binding[1]}\a') + @staticmethod + def stop_schedulers(schedulers): + for scheduler in schedulers: + scheduler.shutdown() + Logger.info('Background schedulers stopped.') - while WORLD_ON: # sck.accept() is a blocking call, we can't exit this loop gracefully. - # noinspection PyBroadException + @staticmethod + def start(running): + WorldLoader.load_data() + + # Start background tasks. + schedulers = WorldServerSessionHandler.build_get_schedulers() + WorldServerSessionHandler.start_schedulers(schedulers) + + # Chat logger. + WorldServerSessionHandler.start_chat_logger() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: try: - client_socket, client_address = server_socket.accept() - server_handler = WorldServerSessionHandler(client_socket, client_address) - world_session_thread = threading.Thread(target=server_handler.handle) - world_session_thread.daemon = True - world_session_thread.start() - except: - break + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + # Use SO_REUSEADDR if SO_REUSEPORT doesn't exist. + except AttributeError: + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind((config.Server.Connection.WorldServer.host, config.Server.Connection.WorldServer.port)) + server_socket.listen() + server_socket.settimeout(2) + + real_binding = server_socket.getsockname() + Logger.success(f'World server started, listening on {real_binding[0]}:{real_binding[1]}\a') + + while WORLD_ON and running.value: + try: + client_socket, client_address = server_socket.accept() + server_handler = WorldServerSessionHandler(client_socket, client_address) + world_session_thread = threading.Thread(target=server_handler.handle) + world_session_thread.daemon = True + world_session_thread.start() + except socket.timeout: + pass # Non blocking. + except OSError: + Logger.warning(traceback.format_exc()) + except KeyboardInterrupt: + break + + Logger.info("World server turned off.") + ChatLogManager.exit() + WorldServerSessionHandler.stop_schedulers(schedulers) diff --git a/game/world/managers/CommandManager.py b/game/world/managers/CommandManager.py index 7f0fd7f14..8c4274572 100644 --- a/game/world/managers/CommandManager.py +++ b/game/world/managers/CommandManager.py @@ -15,7 +15,7 @@ from utils.ConfigManager import config from utils.GitUtils import GitUtils from utils.TextUtils import GameTextFormatter -from utils.constants.MiscCodes import UnitDynamicTypes, MoveFlags +from utils.constants.MiscCodes import UnitDynamicTypes, MoveFlags, ObjectTypeIds from utils.constants.SpellCodes import SpellEffects, SpellTargetMask from utils.constants.UnitCodes import UnitFlags, WeaponMode from utils.constants.UpdateFields import PlayerFields @@ -101,6 +101,11 @@ def help(world_session, args): return 0, f'{total_number} commands found.' + @staticmethod + def toggle_collision(world_session, args): + active = world_session.player_mgr.toggle_collision() + return 0, f'collision cheat {'On' if active else 'Off'}' + @staticmethod def speed(world_session, args): try: @@ -679,6 +684,18 @@ def unmount(world_session, args): player_mgr.unmount() return 0, '' + @staticmethod + def setvirtualitem(world_session, args): + try: + item_id = int(args) + unit = CommandManager._target_or_self(world_session) + if unit.get_type_id() != ObjectTypeIds.ID_UNIT: + return -1, 'target must be unit.' + unit.set_virtual_equipment(slot=0, item_id=item_id) + return 0, '' + except ValueError: + return -1, 'please specify a valid display id.' + @staticmethod def morph(world_session, args): try: @@ -996,6 +1013,26 @@ def createmonster(world_session, args): return 0, '' + @staticmethod + def mapstats(world_session, args): + from game.world.managers.maps.MapManager import MapManager + tile_count = MapManager.get_active_tiles_count() + maps = MapManager.get_active_maps() + result = f'Total active tiles/adts: {tile_count}\n' + for m in maps: + result += (f'Id:{m.map_id}, ' + f'Name:{m.name}, ' + f'InstanceId:{m.instance_id}, ' + f'Active Cells: {m.get_active_cell_count()}\n' + ) + return 0, result + + @staticmethod + def deactivate_cells(world_session, args): + from game.world.managers.maps.MapManager import MapManager + MapManager.deactivate_cells() + return 0, '' + @staticmethod def destroymonster(world_session, args): try: @@ -1066,6 +1103,7 @@ def gmtag(world_session, args): # noinspection SpellCheckingInspection GM_COMMAND_DEFINITIONS = { + 'collision': [CommandManager.toggle_collision, 'toggle collision'], 'speed': [CommandManager.speed, 'change your run speed'], 'swimspeed': [CommandManager.swim_speed, 'change your swim speed'], 'scriptwp': [CommandManager.activate_script_waypoints, 'tries to activate the selected unit script waypoints'], @@ -1104,6 +1142,7 @@ def gmtag(world_session, args): 'unmount': [CommandManager.unmount, 'dismount'], 'morph': [CommandManager.morph, 'morph the targeted unit'], 'demorph': [CommandManager.demorph, 'demorph the targeted unit'], + 'setvirtualitem': [CommandManager.setvirtualitem, 'equips virtual item on unit main hand'], 'cinfo': [CommandManager.creature_info, 'get targeted creature info'], 'unitflags': [CommandManager.unit_flags, 'get targeted unit flags status'], 'weaponmode': [CommandManager.weaponmode, 'set targeted creature weapon mode'], @@ -1125,6 +1164,8 @@ def gmtag(world_session, args): } DEV_COMMAND_DEFINITIONS = { + 'mapstats': [CommandManager.mapstats, 'active maps, adts and cells'], + 'deactivatecells': [CommandManager.deactivate_cells, 'run cell deactivate process'], 'destroymonster': [CommandManager.destroymonster, 'destroy the selected creature'], 'createmonster': [CommandManager.createmonster, 'spawn a creature at your position'], 'sloc': [CommandManager.save_location, 'save your location to locations.log along with a comment'], diff --git a/game/world/managers/maps/Cell.py b/game/world/managers/maps/Cell.py index 3018c9e9f..4eb5b2402 100644 --- a/game/world/managers/maps/Cell.py +++ b/game/world/managers/maps/Cell.py @@ -1,4 +1,5 @@ from game.world.managers.maps.helpers.CellUtils import VIEW_DISTANCE +from game.world.managers.maps.helpers.MapUtils import MapUtils from game.world.managers.objects.farsight.FarSightManager import FarSightManager from utils.constants.MiscCodes import ObjectTypeIds from threading import RLock @@ -15,6 +16,8 @@ def __init__(self, min_x=0.0, min_y=0.0, max_x=0.0, max_y=0.0, map_id=0, instanc self.map_id = map_id self.instance_id = instance_id self.key = key + self.adt_x, self.adt_y = MapUtils.get_tile(self.mid_x, self.mid_y) + self.adt_key = f'{self.adt_x},{self.adt_y}' # Cell lock. self.cell_lock = RLock() # Instances. @@ -31,6 +34,11 @@ def __init__(self, min_x=0.0, min_y=0.0, max_x=0.0, max_y=0.0, map_id=0, instanc self.key = (f'{round(self.min_x, 5)}:{round(self.min_y, 5)}:{round(self.max_x, 5)}:{round(self.max_y, 5)}:' f'{self.map_id}:{self.instance_id}') + self.hash = hash(self.key) + + def __hash__(self): + return self.hash + def get_players(self, caller, visibility_range=True): return {k: v for k, v in list(self.players.items()) if (visibility_range and Cell._object_in_visible_range(caller, v)) or not visibility_range} @@ -223,3 +231,7 @@ def send_all_in_range(self, packet, range_, source, include_source=True, exclude # If this cell has cameras, route packets. for camera in FarSightManager.get_cell_cameras(self): camera.broadcast_packet(packet, exclude=players_reached) + + def can_deactivate(self): + return not self.has_players() and not self.has_cameras() + diff --git a/game/world/managers/maps/GridManager.py b/game/world/managers/maps/GridManager.py index bb7237622..119dd0020 100644 --- a/game/world/managers/maps/GridManager.py +++ b/game/world/managers/maps/GridManager.py @@ -10,13 +10,14 @@ class GridManager: - def __init__(self, map_id, instance_id, active_cell_callback): + def __init__(self, map_id, instance_id, active_cell_callback, inactive_cell_callback): self.map_id = map_id self.grid_lock = RLock() self.instance_id = instance_id self.active_cell_keys: set[str] = set() self.cells: dict[str, Cell] = {} self.active_cell_callback = active_cell_callback + self.inactive_cell_callback = inactive_cell_callback def spawn_object(self, world_object_spawn=None, world_object_instance=None): if world_object_instance: @@ -29,6 +30,7 @@ def spawn_object(self, world_object_spawn=None, world_object_instance=None): def update_object(self, world_object, has_changes=False, has_inventory_changes=False): source_cell_key = world_object.current_cell current_cell_key = CellUtils.get_cell_key_for_object(world_object) + cell_swap = source_cell_key is not None and current_cell_key != source_cell_key # Handle cell change within the same map. if current_cell_key != source_cell_key: @@ -64,8 +66,8 @@ def update_object(self, world_object, has_changes=False, has_inventory_changes=F # Notify cell changed if needed. if current_cell_key != source_cell_key: - if current_cell_key not in self.active_cell_keys: - self._activate_cell_by_world_object(world_object) + if current_cell_key not in self.active_cell_keys and cell_swap: + self.activate_cell_by_world_object(world_object) Logger.debug(f'Unit {world_object.get_name()} triggered inactive cell {current_cell_key}') world_object.on_cell_change() @@ -88,21 +90,33 @@ def is_active_cell_for_location(self, location): cell_key = CellUtils.get_cell_key(location.x, location.y, self.map_id, self.instance_id) return self.is_active_cell(cell_key) - # TODO: Should cleanup loaded tiles for deactivated cells. + def get_active_cell_count(self): + return len(self.active_cell_keys) + def deactivate_cells(self): with self.grid_lock: + adt_keys = dict() for cell_key in list(self.active_cell_keys): - players_near = False - for cell in self._get_surrounding_cells_by_cell(self.cells[cell_key]): - if cell.has_players() or cell.has_cameras(): - players_near = True - break # Make sure only Cells with no players near are removed from the Active list. - if not players_near: - cell = self.cells[cell_key] - self.active_cell_keys.discard(cell_key) - cell.stop_movement() + if any(not cell.can_deactivate() for cell in self._get_surrounding_cells_by_cell(self.cells[cell_key])): + continue + + cell = self.cells[cell_key] + self.active_cell_keys.discard(cell_key) + cell.stop_movement() + + if cell.adt_key not in adt_keys: + adt_keys[cell.adt_key] = set() + adt_keys[cell.adt_key].add(cell) + + for adt_key, cells in adt_keys.items(): + # Check if there are active cells still using the affected adt tile. + if any(cell.adt_key == adt_key for cell in self.cells.values() if cell.key in self.active_cell_keys): + continue + # No active cells for given adt, unload. + adt_x, adt_y = adt_key.split(',') + self.inactive_cell_callback(self.map_id, int(adt_x), int(adt_y)) def _add_world_object_spawn(self, world_object_spawn): cell = self._get_create_cell(world_object_spawn.location, world_object_spawn.map_id, @@ -114,7 +128,7 @@ def _add_world_object(self, world_object, update_players=True): cell.add_world_object(world_object) if world_object.get_type_id() == ObjectTypeIds.ID_PLAYER: - self._activate_cell_by_world_object(world_object) + self.activate_cell_by_world_object(world_object) # Notify surrounding players. if update_players: @@ -126,26 +140,24 @@ def _add_world_object(self, world_object, update_players=True): self._update_players_surroundings(cell.key, object_type=world_object.get_type_id()) - def _activate_cell_by_world_object(self, world_object): - affected_cells = list(self._get_surrounding_cells_by_object(world_object)) - # Try to load tile maps for affected cells if needed. - self._load_maps_for_cells(affected_cells) - # Try to activate this player cell. - self.active_cell_callback(world_object) - # Set affected cells as active cells based on creatures if needed. - self._activate_cells(affected_cells) + def activate_cell_by_world_object(self, world_object): + # Surrounding cells. + affected_cells = set(self._get_surrounding_cells_by_cell(self.cells[world_object.current_cell])) + # Self cell. + affected_cells.add(self.cells[world_object.current_cell]) + self._activate_cells(affected_cells, world_object) - def _activate_cells(self, cells: list[Cell]): + def _activate_cells(self, cells: set[Cell], world_object): with self.grid_lock: - for cell in cells: - if cell.key not in self.active_cell_keys: - self.active_cell_keys.add(cell.key) + [self._activate_cell(cell, world_object) for cell in cells] - def _load_maps_for_cells(self, cells): - for cell in cells: - if cell.key not in self.active_cell_keys: - for creature in list(cell.creatures.values()): - self.active_cell_callback(creature) + def _activate_cell(self, cell, world_object): + if cell.key not in self.active_cell_keys: + self.active_cell_keys.add(cell.key) + + # Only player activation will trigger tile/nav loading. + if world_object.get_type_id() == ObjectTypeIds.ID_PLAYER: + self.active_cell_callback(self.map_id, cell.adt_x, cell.adt_y) def _update_players_surroundings(self, cell_key, exclude_cells=None, world_object=None, has_changes=False, has_inventory_changes=False, update_data=None, object_type=None): diff --git a/game/world/managers/maps/Map.py b/game/world/managers/maps/Map.py index 765a5b4b1..a0c626a49 100644 --- a/game/world/managers/maps/Map.py +++ b/game/world/managers/maps/Map.py @@ -1,3 +1,4 @@ +from game.world.managers.maps.helpers.MapUtils import MapUtils from game.world.managers.objects.pools.PoolManager import PoolManager from utils.ConfigManager import config from utils.Logger import Logger @@ -12,13 +13,13 @@ class Map: - def __init__(self, map_id, active_cell_callback, instance_id, map_manager): + def __init__(self, map_id, active_cell_callback, inactive_cell_callback, instance_id, map_manager): self.map_id = map_id self.map_manager = map_manager self.dbc_map = DbcDatabaseManager.map_get_by_id(map_id) self.instance_id = instance_id self.name = self.dbc_map.MapName_enUS - self.grid_manager = GridManager(map_id, instance_id, active_cell_callback) + self.grid_manager = GridManager(map_id, instance_id, active_cell_callback, inactive_cell_callback) self.script_handler = ScriptHandler(self) self.map_event_manager = MapEventManager() self.pool_manager = PoolManager() @@ -92,7 +93,8 @@ def _load_gameobjects_pools_data(self, gobject_spawns): length = len(gobject_spawns) for gobject_spawn in gobject_spawns: go_spawn_instance = GameObjectSpawn(gobject_spawn, instance_id=self.instance_id) - go_spawn_instance.generate_or_add_to_pool_if_needed(self.pool_manager) + if config.Server.Settings.load_pools: + go_spawn_instance.generate_or_add_to_pool_if_needed(self.pool_manager) if not go_spawn_instance.pool: go_spawn_instances.append(go_spawn_instance) count += 1 @@ -108,7 +110,8 @@ def _load_creatures_pools_data(self, creature_spawns): length = len(creature_spawns) for creature_spawn in creature_spawns: creature_spawn_instance = CreatureSpawn(creature_spawn, instance_id=self.instance_id) - creature_spawn_instance.generate_or_add_to_pool_if_needed(self.pool_manager) + if config.Server.Settings.load_pools: + creature_spawn_instance.generate_or_add_to_pool_if_needed(self.pool_manager) if not creature_spawn_instance.pool: creature_spawn_instances.append(creature_spawn_instance) count += 1 @@ -203,7 +206,7 @@ def los_check(self, start_vector, end_vector, doodads=False): return self.map_manager.los_check(self.map_id, start_vector, end_vector, doodads=doodads) def get_tile(self, x, y): - return self.map_manager.get_tile(x, y) + return MapUtils.get_tile(x, y) # GridManager helpers. @@ -270,6 +273,12 @@ def is_active_cell(self, cell_coords): def is_active_cell_for_location(self, location): return self.grid_manager.is_active_cell_for_location(location) + def get_active_cell_count(self): + return self.grid_manager.get_active_cell_count() + + def activate_cell_by_world_object(self, world_object): + self.grid_manager.activate_cell_by_world_object(world_object) + # Objects updates. def update_creatures(self): self.grid_manager.update_creatures() diff --git a/game/world/managers/maps/MapManager.py b/game/world/managers/maps/MapManager.py index d7d00998d..b3b86fff2 100644 --- a/game/world/managers/maps/MapManager.py +++ b/game/world/managers/maps/MapManager.py @@ -13,19 +13,21 @@ from game.world.managers.maps.helpers.Constants import ADT_SIZE, RESOLUTION_ZMAP, RESOLUTION_AREA_INFO, \ RESOLUTION_LIQUIDS, BLOCK_SIZE from game.world.managers.maps.Map import Map, MapType -from game.world.managers.maps.MapTile import MapTile, MapTileStates +from game.world.managers.maps.MapTile import MapTile from game.world.managers.maps.helpers.LiquidInformation import LiquidInformation +from game.world.managers.maps.helpers.MapUtils import MapUtils from game.world.managers.maps.helpers.Namigator import Namigator from utils.ConfigManager import config from utils.Logger import Logger from utils.PathManager import PathManager -from utils.constants.MiscCodes import MapsNoNavs +from utils.constants.MiscCodes import MapsNoNavs, MapTileStates + MAPS: dict[int, dict[int, Map]] = {} MAP_LIST: list[int] = DbcDatabaseManager.map_get_all_ids() # Holds .map files tiles information per Map. MAPS_TILES = dict() -# Holds namigator instances per Map. +# Holds namigator instances, one instance per common Map. MAPS_NAMIGATOR: dict[int, Namigator] = dict() AREAS = {} @@ -71,7 +73,7 @@ def _build_map(map_id, instance_id): if map_id not in MAPS: MAPS[map_id] = dict() - new_map = Map(map_id, MapManager.on_cell_turn_active, instance_id=instance_id, map_manager=MapManager) + new_map = Map(map_id, MapManager.on_cell_turn_active, MapManager.on_cell_turn_inactive, instance_id=instance_id, map_manager=MapManager) MAPS[map_id][instance_id] = new_map new_map.initialize() MapManager._build_map_namigator(new_map) @@ -102,16 +104,26 @@ def _build_map_namigator(map_: Map): return @staticmethod - def enqueue_adt_tile_load(map_id, raw_x, raw_y): - with QUEUE_LOCK: - adt_x, adt_y = MapManager.get_tile(raw_x, raw_y) + def get_active_maps(): + active_maps = [] + for map_id, instances in list(MAPS.items()): + for instance_map in list(instances.values()): + active_maps.append(instance_map) + return active_maps + @staticmethod + def get_active_tiles_count(): + return len([loaded for loaded in PENDING_TILE_INITIALIZATION.values() if loaded]) + + @staticmethod + def enqueue_adt_tile_load(map_id, adt_x, adt_y): + with QUEUE_LOCK: adt_key = f'{map_id},{adt_x},{adt_y}' if adt_key in PENDING_TILE_INITIALIZATION: return PENDING_TILE_INITIALIZATION[adt_key] = True - to_load_data = f'{map_id},{raw_x},{raw_y}' + to_load_data = f'{map_id},{adt_x},{adt_y}' PENDING_TILE_INITIALIZATION_QUEUE.put(to_load_data) @staticmethod @@ -119,7 +131,7 @@ def _build_map_adt_tiles(map_: Map): if map_.map_id in MAPS_TILES: return - MAPS_TILES[map_.map_id] = [[None for r in range(64)] for c in range(64)] + MAPS_TILES[map_.map_id] = [[None for _ in range(BLOCK_SIZE)] for _ in range(BLOCK_SIZE)] for adt_x in range(BLOCK_SIZE): for adt_y in range(BLOCK_SIZE): MAPS_TILES[map_.map_id][adt_x][adt_y] = MapTile(map_, adt_x, adt_y) @@ -132,26 +144,21 @@ def initialize_pending_tiles(): if PENDING_TILE_INITIALIZATION_QUEUE.empty(): return key = PENDING_TILE_INITIALIZATION_QUEUE.get() - map_id, x, y = str(key).rsplit(',') - MapManager.initialize_adt_tile(int(map_id), float(x), float(y)) + map_id, adt_x, adt_y = str(key).rsplit(',') + MapManager.initialize_adt_tile(int(map_id), int(adt_x), int(adt_y)) @staticmethod - def initialize_adt_tile(map_id, x, y): + def initialize_adt_tile(map_id, adt_x, adt_y): if map_id not in MAP_LIST: return False - adt_x, adt_y = MapManager.get_tile(x, y) - # Map namigator instance, if available. - namigator = MAPS_NAMIGATOR[map_id] if map_id in MAPS_NAMIGATOR and MapManager.NAMIGATOR_LOADED else None + namigator = MAPS_NAMIGATOR[map_id] if (map_id in MAPS_NAMIGATOR and MapManager.NAMIGATOR_LOADED) else None + + if MAPS_TILES[map_id][adt_x][adt_y].initialized: + return False - for i in range(-1, 1): - for j in range(-1, 1): - if -1 < adt_x + i < 64 and -1 < adt_y + j < 64: - if MAPS_TILES[map_id][adt_x + i][adt_y + j].initialized: - continue - Logger.debug(f'[Map] Loading ADT tile {adt_x + i},{adt_y + j}') - MAPS_TILES[map_id][adt_x + i][adt_y + j].initialize(namigator) + MAPS_TILES[map_id][adt_x][adt_y].initialize(namigator) return True @@ -161,7 +168,7 @@ def get_map(map_id, instance_id) -> Optional[Map]: return MAPS[map_id][instance_id] except (KeyError, AttributeError, TypeError): Logger.error(f'Unable to retrieve Map for Id {map_id}, Instance {instance_id}') - return None + return None @staticmethod def get_or_create_instance_map(instance_token) -> Map: @@ -201,11 +208,29 @@ def get_liquid_or_create(liquid_type, height, use_float_16): return LIQUIDS_CACHE[key] @staticmethod - def on_cell_turn_active(world_object): - MapManager.enqueue_adt_tile_load(world_object.map_id, world_object.location.x, world_object.location.y) + def on_cell_turn_active(map_id, adt_x, adt_y): + MapManager.enqueue_adt_tile_load(map_id, adt_x, adt_y) @staticmethod - def validate_maps(): + def on_cell_turn_inactive(map_id, adt_x, adt_y): + # Normal Tile unload (.map) + tile = MAPS_TILES[map_id][adt_x][adt_y] + if tile and tile.is_ready(): + Logger.info(f'[Map] Unloading map tile, Map:{map_id} Tile:{adt_x},{adt_y}') + tile.unload() + + # Namigator unload (.nav) + if (map_id in MAPS_NAMIGATOR and MapManager.has_navs(map_id) and not MapManager.NAMIGATOR_FAILED + and config.Server.Settings.use_nav_tiles) and MAPS_NAMIGATOR[map_id].adt_loaded(adt_y, adt_x): + MAPS_NAMIGATOR[map_id].unload_adt(adt_y, adt_x) + Logger.info(f'[Namigator] Unloading nav ADT, Map:{map_id} Tile:{adt_x},{adt_y}') + + adt_key = f'{map_id},{adt_x},{adt_y}' + if adt_key in PENDING_TILE_INITIALIZATION: + del PENDING_TILE_INITIALIZATION[adt_key] + + @staticmethod + def validate_map_files(): if not config.Server.Settings.use_map_tiles: return True @@ -215,25 +240,50 @@ def validate_maps(): return True @staticmethod - def calculate_tile(x, y, resolution=RESOLUTION_ZMAP - 1): - x = MapManager.validate_map_coord(x) - y = MapManager.validate_map_coord(y) - adt_x = int(32.0 - (x / ADT_SIZE)) - adt_y = int(32.0 - (y / ADT_SIZE)) - cell_x = int(round(resolution * (32.0 - (x / ADT_SIZE) - adt_x))) - cell_y = int(round(resolution * (32.0 - (y / ADT_SIZE) - adt_y))) - return adt_x, adt_y, cell_x, cell_y + def calculate_z_for_object(w_object): + return MapManager.calculate_z(w_object.map_id, w_object.location.x, w_object.location.y, w_object.location.z) + # noinspection PyBroadException @staticmethod - def get_tile(x, y): - adt_x = int(32.0 - MapManager.validate_map_coord(x) / ADT_SIZE) - adt_y = int(32.0 - MapManager.validate_map_coord(y) / ADT_SIZE) - return [adt_x, adt_y] + def calculate_z(map_id, x, y, current_z=0.0, is_rand_point=False) -> tuple: + # float, z_locked (Could not use map files Z) + if not config.Server.Settings.use_nav_tiles and not config.Server.Settings.use_map_tiles: + return current_z, False + try: + adt_x, adt_y, cell_x, cell_y = MapUtils.calculate_tile(x, y) - @staticmethod - def calculate_nav_z_for_object(world_object): - return MapManager.calculate_nav_z(world_object.map_id, world_object.location.x, world_object.location.y, - world_object.location.z) + # No tile data available or busy loading. + if MapManager._check_tile_load(map_id, x, y, adt_x, adt_y) != MapTileStates.READY: + return current_z, False + + # Always prioritize Namigator if enabled. + if config.Server.Settings.use_nav_tiles: + nav_z, z_locked = MapManager.calculate_nav_z(map_id, x, y, current_z, is_rand_point=is_rand_point) + if not z_locked: + return nav_z, False + + # Check if we have .map data for this request. + tile = MAPS_TILES[map_id][adt_x][adt_y] + if not tile or not tile.has_maps: + return current_z, True + + try: + calculated_z = MapManager.get_normalized_height_for_cell(map_id, x, y, adt_x, adt_y, cell_x, cell_y) + # Tolerance. + tol = 1.1 if not is_rand_point else 2 + # If Z goes outside boundaries, expand our search. + if (math.fabs(current_z - calculated_z) > tol) and current_z: + found, z2 = MapManager.get_near_height(map_id, x, y, adt_x, adt_y, cell_x, cell_y, current_z, tol) + # Not locked if found, else current z locked. + return (z2, False) if found else (current_z, True) + # First Z was valid. + return calculated_z, False + except: + tile = MAPS_TILES[map_id][adt_x][adt_y] + return tile.z_height_map[cell_x][cell_x], False + except: + Logger.error(traceback.format_exc()) + return current_z if current_z else 0.0, False @staticmethod def calculate_nav_z(map_id, x, y, current_z=0.0, is_rand_point=False) -> tuple: # float, bool result negation @@ -247,7 +297,7 @@ def calculate_nav_z(map_id, x, y, current_z=0.0, is_rand_point=False) -> tuple: if map_id not in MAPS_NAMIGATOR: return current_z, True - adt_x, adt_y = MapManager.get_tile(x, y) + adt_x, adt_y = MapUtils.get_tile(x, y) # Check if we need to load adt. if MapManager._check_tile_load(map_id, x, y, adt_x, adt_y) != MapTileStates.READY: return current_z, True @@ -282,10 +332,10 @@ def los_check(map_id, src_loc, dst_loc, doodads=False): return True # Calculate source adt coordinates for x,y. - src_adt_x, src_adt_y = MapManager.get_tile(src_loc.x, src_loc.y) + src_adt_x, src_adt_y = MapUtils.get_tile(src_loc.x, src_loc.y) # Calculate destination adt coordinates for x,y. - dst_adt_x, dst_adt_y = MapManager.get_tile(dst_loc.x, dst_loc.y) + dst_adt_x, dst_adt_y = MapUtils.get_tile(dst_loc.x, dst_loc.y) # Check if loaded or unable to load, return True if this fails. initial_source_tile_state = MapManager._check_tile_load(map_id, src_loc.x, src_loc.y, src_adt_x, src_adt_y) @@ -343,10 +393,10 @@ def calculate_path(map_id, src_loc, dst_loc, los=False) -> tuple: # bool failed return False, False, [dst_loc] # Calculate source adt coordinates for x,y. - src_adt_x, src_adt_y = MapManager.get_tile(src_loc.x, src_loc.y) + src_adt_x, src_adt_y = MapUtils.get_tile(src_loc.x, src_loc.y) # Calculate destination adt coordinates for x,y. - dst_adt_x, dst_adt_y = MapManager.get_tile(dst_loc.x, dst_loc.y) + dst_adt_x, dst_adt_y = MapUtils.get_tile(dst_loc.x, dst_loc.y) # Check if loaded or unable to load. if MapManager._check_tile_load(map_id, src_loc.x, src_loc.y, src_adt_x, src_adt_y) != MapTileStates.READY: @@ -393,70 +443,12 @@ def validate_teleport_destination(map_id, x, y): if map_id > 1: return True - adt_x, adt_y = MapManager.get_tile(x, y) - if MapManager._check_tile_load(map_id, x, y, adt_x, adt_y) == MapTileStates.UNUSABLE: + # Validate position is within map boundaries. + if not MapUtils.is_valid_position(x, y): return False return True - @staticmethod - def validate_map_coord(coord): - if coord > 32.0 * ADT_SIZE: - return 32.0 * ADT_SIZE - elif coord < -32.0 * ADT_SIZE: - return -32.0 * ADT_SIZE - return coord - - @staticmethod - def calculate_z_for_object(w_object): - return MapManager.calculate_z(w_object.map_id, w_object.location.x, w_object.location.y, w_object.location.z) - - # noinspection PyBroadException - @staticmethod - def calculate_z(map_id, x, y, current_z=0.0, is_rand_point=False) -> tuple: - # float, z_locked (Could not use map files Z) - if not config.Server.Settings.use_nav_tiles and not config.Server.Settings.use_map_tiles: - return current_z, False - try: - # Check both axis within boundaries. - x = MapManager.validate_map_coord(x) - y = MapManager.validate_map_coord(y) - - adt_x, adt_y, cell_x, cell_y = MapManager.calculate_tile(x, y) - - # No tile data available or busy loading. - if MapManager._check_tile_load(map_id, x, y, adt_x, adt_y) != MapTileStates.READY: - return current_z, False - - # Always prioritize Namigator if enabled. - if config.Server.Settings.use_nav_tiles: - nav_z, z_locked = MapManager.calculate_nav_z(map_id, x, y, current_z, is_rand_point=is_rand_point) - if not z_locked: - return nav_z, False - - # Check if we have .map data for this request. - tile = MAPS_TILES[map_id][adt_x][adt_y] - if not tile or not tile.has_maps: - return current_z, True - - try: - calculated_z = MapManager.get_normalized_height_for_cell(map_id, x, y, adt_x, adt_y, cell_x, cell_y) - # Tolerance. - tol = 1.1 if not is_rand_point else 2 - # If Z goes outside boundaries, expand our search. - if (math.fabs(current_z - calculated_z) > tol) and current_z: - found, z2 = MapManager.get_near_height(map_id, x, y, adt_x, adt_y, cell_x, cell_y, current_z, tol) - # Not locked if found, else current z locked. - return (z2, False) if found else (current_z, True) - # First Z was valid. - return calculated_z, False - except: - tile = MAPS_TILES[map_id][adt_x][adt_y] - return tile.z_height_map[cell_x][cell_x], False - except: - Logger.error(traceback.format_exc()) - return current_z if current_z else 0.0, False - @staticmethod def get_cell_height(map_id, adt_x, adt_y, cell_x, cell_y): if cell_x > RESOLUTION_ZMAP: @@ -502,7 +494,7 @@ def get_area_information(map_id, x, y): if not config.Server.Settings.use_map_tiles: return None try: - adt_x, adt_y, cell_x, cell_y = MapManager.calculate_tile(x, y, RESOLUTION_AREA_INFO - 1) + adt_x, adt_y, cell_x, cell_y = MapUtils.calculate_tile(x, y, RESOLUTION_AREA_INFO - 1) if MapManager._check_tile_load(map_id, x, y, adt_x, adt_y) != MapTileStates.READY: return None @@ -519,7 +511,7 @@ def get_liquid_information(map_id, x, y, z, ignore_z=False): if not config.Server.Settings.use_map_tiles: return None try: - adt_x, adt_y, cell_x, cell_y = MapManager.calculate_tile(x, y, RESOLUTION_LIQUIDS - 1) + adt_x, adt_y, cell_x, cell_y = MapUtils.calculate_tile(x, y, RESOLUTION_LIQUIDS - 1) if MapManager._check_tile_load(map_id, x, y, adt_x, adt_y) != MapTileStates.READY: return None @@ -559,7 +551,7 @@ def find_liquid_location_in_range(world_object, min_range, max_range): @staticmethod def _validate_liquid_tile(map_id, x, y): - adt_x, adt_y = MapManager.get_tile(x, y) + adt_x, adt_y = MapUtils.get_tile(x, y) if MapManager._check_tile_load(map_id, x, y, adt_x, adt_y) != MapTileStates.READY: return False return True @@ -577,9 +569,6 @@ def _check_tile_load(map_id, location_x, location_y, adt_x, adt_y): # Loaded but has no maps or navs data. elif tile.is_ready() and not tile.can_use(): return MapTileStates.UNUSABLE - elif not tile.is_loading() and not tile.is_initialized(): - MapManager.enqueue_adt_tile_load(map_id, location_x, location_y) - return MapTileStates.LOADING # Initialized but still loading. elif tile.is_loading(): return MapTileStates.LOADING diff --git a/game/world/managers/maps/MapTile.py b/game/world/managers/maps/MapTile.py index 5d3d5f191..88b7151fe 100644 --- a/game/world/managers/maps/MapTile.py +++ b/game/world/managers/maps/MapTile.py @@ -1,6 +1,5 @@ import os import traceback -from enum import IntEnum from os import path from struct import unpack @@ -13,12 +12,6 @@ from utils.PathManager import PathManager -class MapTileStates(IntEnum): - READY = 0 - LOADING = 1 - UNUSABLE = 2 - - class MapTile(object): EXPECTED_VERSION = 'ACMAP_1.70' @@ -71,37 +64,41 @@ def initialize(self, namigator): self.has_navigation = self.load_namigator_data(namigator) self.ready = True + def unload(self): + self.initialized = False + self.has_maps = False + self.has_navigation = False + self.ready = False + self.area_information = None + self.liquid_information = None + self.z_height_map = None + def load_namigator_data(self, namigator): if not config.Server.Settings.use_nav_tiles or not namigator: return False + return self._load_namigator_adt_navs(namigator) - if not self.map_.is_dungeon() and namigator.has_adts(): - return self._load_namigator_adt(namigator) - else: - return self._load_namigator_wmo_map(namigator) - - def _load_namigator_wmo_map(self, namigator): - Logger.debug(f'[Namigator] Loading nav WMO, Map:{self.map_id} Tile:{self.adt_x},{self.adt_y}') - self.has_navigation = True - return True - - def _load_namigator_adt(self, namigator): + def _load_namigator_adt_navs(self, namigator): try: - Logger.debug(f'[Namigator] Loading nav ADT, Map:{self.map_id} Tile:{self.adt_x},{self.adt_y}') - # Notice, namigator has inverted coordinates. - namigator.load_adt(self.adt_y, self.adt_x) - self.has_navigation = True - return True + Logger.info(f'[Namigator] Loading navs, Map:{self.map_id} Tile:{self.adt_x},{self.adt_y}') + if namigator.has_adts(): + # Notice, namigator has inverted coordinates. + if not namigator.adt_loaded(self.adt_y, self.adt_x): + namigator.load_adt(self.adt_y, self.adt_x) + self.has_navigation = namigator.adt_loaded(self.adt_y, self.adt_x) + else: # WMO only. + self.has_navigation = True + return self.has_navigation except RuntimeError: Logger.error(traceback.format_exc()) return False def load_maps_data(self): - if not config.Server.Settings.use_map_tiles or self.map_.is_dungeon(): # No .map for dungeons. + if not config.Server.Settings.use_map_tiles or self.map_.is_dungeon(): # No .map files for dungeons. return False filename = f'{self.map_id:03}{self.adt_x:02}{self.adt_y:02}.map' maps_path = PathManager.get_map_file_path(filename) - Logger.debug(f'[Maps] Loading map file: {filename}, Map:{self.map_id} Tile:{self.adt_x},{self.adt_y}') + Logger.info(f'[Maps] Loading map tile, Map:{self.map_id} Tile:{self.adt_x},{self.adt_y}, File: {filename}') if not path.exists(maps_path): Logger.warning(f'[Maps] Unable to locate map file: {filename}, ' diff --git a/game/world/managers/maps/helpers/MapUtils.py b/game/world/managers/maps/helpers/MapUtils.py new file mode 100644 index 000000000..5cb929d71 --- /dev/null +++ b/game/world/managers/maps/helpers/MapUtils.py @@ -0,0 +1,37 @@ +from game.world.managers.maps.helpers.Constants import RESOLUTION_ZMAP, ADT_SIZE + + +class MapUtils: + + @staticmethod + def calculate_tile(x, y, resolution=RESOLUTION_ZMAP - 1): + # Check both axis within boundaries. + x = MapUtils._validate_map_coord(x) + y = MapUtils._validate_map_coord(y) + adt_x = int(32.0 - (x / ADT_SIZE)) + adt_y = int(32.0 - (y / ADT_SIZE)) + cell_x = int(round(resolution * (32.0 - (x / ADT_SIZE) - adt_x))) + cell_y = int(round(resolution * (32.0 - (y / ADT_SIZE) - adt_y))) + return adt_x, adt_y, cell_x, cell_y + + @staticmethod + def get_tile(x, y): + adt_x = int(32.0 - MapUtils._validate_map_coord(x) / ADT_SIZE) + adt_y = int(32.0 - MapUtils._validate_map_coord(y) / ADT_SIZE) + return [adt_x, adt_y] + + @staticmethod + def _validate_map_coord(coord): + if coord > 32.0 * ADT_SIZE: + return 32.0 * ADT_SIZE + elif coord < -32.0 * ADT_SIZE: + return -32.0 * ADT_SIZE + return coord + + @staticmethod + def is_valid_position(x, y): + if x > 32.0 * ADT_SIZE or y > 32.0 * ADT_SIZE: + return False + elif x < -32.0 * ADT_SIZE or y < -32.0 * ADT_SIZE: + return False + return True diff --git a/game/world/managers/maps/helpers/Namigator.py b/game/world/managers/maps/helpers/Namigator.py index 9d551af26..fb9cb78a5 100644 --- a/game/world/managers/maps/helpers/Namigator.py +++ b/game/world/managers/maps/helpers/Namigator.py @@ -19,6 +19,9 @@ def has_adts(self): def adt_loaded(self, adt_x, adt_y): pass + def load_all_adts(self): + pass + def load_adt(self, adt_x, adt_y): pass @@ -29,4 +32,4 @@ def find_random_point_around_circle(self, start_x, start_y, start_z, radius): pass def find_point_in_between_vectors(self, distance, start_x, start_y, start_z, end_x, end_y, end_z): - pass \ No newline at end of file + pass diff --git a/game/world/managers/objects/ObjectManager.py b/game/world/managers/objects/ObjectManager.py index 9b19be9ad..260bba6d1 100644 --- a/game/world/managers/objects/ObjectManager.py +++ b/game/world/managers/objects/ObjectManager.py @@ -57,7 +57,7 @@ def __init__(self, self.current_display_id = native_display_id self.faction = faction self.bounding_radius = bounding_radius - self.location = Vector() + self.location = location if location else Vector() self.transport_id = transport_id self.transport_location = Vector() self.pitch = pitch @@ -75,6 +75,7 @@ def __init__(self, self.last_tick = 0 self.movement_spline = None self.object_ai = None + self.collision_cheat = False # Units and gameobjects have SpellManager. from game.world.managers.objects.spell.SpellManager import SpellManager @@ -209,6 +210,9 @@ def get_stationary_position(self): def get_name(self): return '' + def get_entry(self): + return self.entry + def get_display_id(self): return self.current_display_id @@ -429,6 +433,7 @@ def despawn(self, ttl=0): if self.is_default and not ttl: self.get_map().remove_object(self) return + # TODO: Some objects are being despawned and not entirely destroyed. e.g. Fishing Bobber, Duel Flags, Rituals. # Despawn (De-activate) self.get_map().update_object(self, has_changes=True) diff --git a/game/world/managers/objects/corpse/CorpseManager.py b/game/world/managers/objects/corpse/CorpseManager.py index 317f288db..4e71a7b42 100644 --- a/game/world/managers/objects/corpse/CorpseManager.py +++ b/game/world/managers/objects/corpse/CorpseManager.py @@ -97,6 +97,12 @@ def spawn(player_mgr): def get_name(self): return self.name + # override + def get_entry(self): + if self.entry: + return self.entry + return 0 + # override def get_type_mask(self): return super().get_type_mask() | ObjectTypeFlags.TYPE_CORPSE diff --git a/game/world/managers/objects/dynamic/DynamicObjectManager.py b/game/world/managers/objects/dynamic/DynamicObjectManager.py index 456602f48..6ae260027 100644 --- a/game/world/managers/objects/dynamic/DynamicObjectManager.py +++ b/game/world/managers/objects/dynamic/DynamicObjectManager.py @@ -114,6 +114,12 @@ def is_active_object(self): def get_name(self): return self.name + # override + def get_entry(self): + if self.entry: + return self.entry + return 0 + # override def get_type_mask(self): return super().get_type_mask() | ObjectTypeFlags.TYPE_DYNAMICOBJECT diff --git a/game/world/managers/objects/gameobjects/GameObjectBuilder.py b/game/world/managers/objects/gameobjects/GameObjectBuilder.py index 4a381e301..32b748a31 100644 --- a/game/world/managers/objects/gameobjects/GameObjectBuilder.py +++ b/game/world/managers/objects/gameobjects/GameObjectBuilder.py @@ -1,6 +1,8 @@ +from typing import Callable + from database.world.WorldDatabaseManager import WorldDatabaseManager -from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager from game.world.managers.objects.GuidManager import GuidManager +from utils.constants.MiscCodes import GameObjectTypes, ObjectTypeFlags class GameObjectBuilder: @@ -23,34 +25,85 @@ def create(entry_id, location, map_id, instance_id, state, summoner=None, rot0=0 GameObjectBuilder.MAX_SPAWN_ID += 1 spawn_id = GameObjectBuilder.MAX_SPAWN_ID - gameobject_instance = GameObjectManager() - gameobject_instance.is_default = is_default - gameobject_instance.is_spawned = is_spawned - gameobject_instance.spawn_id = spawn_id - gameobject_instance.is_dynamic_spawn = is_dynamic_spawn - gameobject_instance.entry = gobject_template.entry - gameobject_instance.gobject_template = gobject_template - gameobject_instance.guid = gameobject_instance.generate_object_guid(GameObjectBuilder.GUID_MANAGER.get_new_guid()) - gameobject_instance.map_id = map_id if not summoner else summoner.map_id - gameobject_instance.instance_id = instance_id if not summoner else summoner.instance_id - gameobject_instance.zone = summoner.zone if summoner else 0 - gameobject_instance.summoner = summoner - gameobject_instance.spell_id = spell_id + ctor = GameObjectBuilder.get_object_type_ctor(gobject_template) + go_instance = ctor( + entry=gobject_template.entry, + location=location, + map_id=map_id if not summoner else summoner.map_id, + zone=summoner.zone if summoner else 0) + + go_instance.guid = go_instance.generate_object_guid(GameObjectBuilder.GUID_MANAGER.get_new_guid()) + go_instance.is_default = is_default + go_instance.is_spawned = is_spawned + go_instance.spawn_id = spawn_id + go_instance.is_dynamic_spawn = is_dynamic_spawn + go_instance.gobject_template = gobject_template + go_instance.instance_id = instance_id if not summoner else summoner.instance_id + go_instance.summoner = summoner + go_instance.spell_id = spell_id + go_instance.initial_state = state # Initialize from gameobject template. - gameobject_instance.initialize_from_gameobject_template(gobject_template) + go_instance.initialize_from_gameobject_template(gobject_template) # Continue initialization. (Faction and Flags will be overriden below) - gameobject_instance.faction = faction if faction else gobject_template.faction - gameobject_instance.location = location - gameobject_instance.rot0 = rot0 - gameobject_instance.rot1 = rot1 - gameobject_instance.rot2 = rot2 - gameobject_instance.rot3 = rot3 - gameobject_instance.state = state - gameobject_instance.time_to_live_timer = ttl - - if gameobject_instance.is_transport(): - gameobject_instance.stationary_position = location.copy() - - return gameobject_instance + go_instance.faction = faction if faction else gobject_template.faction + go_instance.location = location + go_instance.rot0 = rot0 + go_instance.rot1 = rot1 + go_instance.rot2 = rot2 + go_instance.rot3 = rot3 + go_instance.time_to_live_timer = ttl + + # Set channel object update field for rituals and fishing nodes. + if (gobject_template.type in {GameObjectTypes.TYPE_RITUAL, GameObjectTypes.TYPE_FISHINGNODE} + and summoner and summoner.get_type_mask() & ObjectTypeFlags.TYPE_UNIT): + summoner.set_channel_object(go_instance.guid) + + return go_instance + + @staticmethod + def get_object_type_ctor(template) -> Callable: + from game.world.managers.objects.gameobjects.managers.DoorManager import DoorManager + from game.world.managers.objects.gameobjects.managers.TrapManager import TrapManager + from game.world.managers.objects.gameobjects.managers.ChairManager import ChairManager + from game.world.managers.objects.gameobjects.managers.ChestMananger import ChestManager + from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager + from game.world.managers.objects.gameobjects.managers.CameraManager import CameraManager + from game.world.managers.objects.gameobjects.managers.GooberManager import GooberManager + from game.world.managers.objects.gameobjects.managers.ButtonManager import ButtonManager + from game.world.managers.objects.gameobjects.managers.RitualManager import RitualManager + from game.world.managers.objects.gameobjects.managers.TransportManager import TransportManager + from game.world.managers.objects.gameobjects.managers.SpellFocusManager import SpellFocusManager + from game.world.managers.objects.gameobjects.managers.MiningNodeManager import MiningNodeManager + from game.world.managers.objects.gameobjects.managers.QuestGiverManager import QuestGiverManager + from game.world.managers.objects.gameobjects.managers.FishingNodeManager import FishingNodeManager + + if template.type == GameObjectTypes.TYPE_DOOR: + return DoorManager + elif template.type == GameObjectTypes.TYPE_BUTTON: + return ButtonManager + elif template.type == GameObjectTypes.TYPE_CHEST and template.data4 != 0 and template.data5 > template.data4: + return MiningNodeManager + elif template.type == GameObjectTypes.TYPE_CHEST: + return ChestManager + elif template.type == GameObjectTypes.TYPE_QUESTGIVER: + return QuestGiverManager + elif template.type == GameObjectTypes.TYPE_TRAP: + return TrapManager + elif template.type == GameObjectTypes.TYPE_FISHINGNODE: + return FishingNodeManager + elif template.type == GameObjectTypes.TYPE_GOOBER: + return GooberManager + elif template.type == GameObjectTypes.TYPE_CHAIR: + return ChairManager + elif template.type == GameObjectTypes.TYPE_CAMERA: + return CameraManager + elif template.type == GameObjectTypes.TYPE_TRANSPORT: + return TransportManager + elif template.type == GameObjectTypes.TYPE_RITUAL: + return RitualManager + elif template.type == GameObjectTypes.TYPE_SPELL_FOCUS: + return SpellFocusManager + else: + return GameObjectManager diff --git a/game/world/managers/objects/gameobjects/GameObjectManager.py b/game/world/managers/objects/gameobjects/GameObjectManager.py index 6e2faf187..2993a725f 100644 --- a/game/world/managers/objects/gameobjects/GameObjectManager.py +++ b/game/world/managers/objects/gameobjects/GameObjectManager.py @@ -1,19 +1,7 @@ import math -from math import pi, cos, sin from struct import pack from database.dbc.DbcDatabaseManager import DbcDatabaseManager -from game.world.managers.abstractions.Vector import Vector -from game.world.managers.objects.gameobjects.managers.ButtonManager import ButtonManager -from game.world.managers.objects.gameobjects.managers.ChestMananger import ChestManager -from game.world.managers.objects.gameobjects.managers.TransportManager import TransportManager -from game.world.managers.objects.gameobjects.managers.FishingNodeManager import FishingNodeManager -from game.world.managers.objects.gameobjects.GameObjectLootManager import GameObjectLootManager -from game.world.managers.objects.gameobjects.managers.GooberManager import GooberManager -from game.world.managers.objects.gameobjects.managers.MiningNodeManager import MiningNodeManager -from game.world.managers.objects.gameobjects.managers.RitualManager import RitualManager -from game.world.managers.objects.gameobjects.managers.SpellFocusManager import SpellFocusManager -from game.world.managers.objects.gameobjects.managers.TrapManager import TrapManager from game.world.managers.objects.ObjectManager import ObjectManager from game.world.managers.objects.GuidManager import GuidManager from network.packet.PacketWriter import PacketWriter @@ -23,12 +11,9 @@ from utils.constants.MiscFlags import GameObjectFlags from utils.constants.OpCodes import OpCode from utils.constants.SpellCodes import SpellMissReason -from utils.constants.UnitCodes import StandState from utils.constants.UpdateFields import ObjectFields, GameObjectFields -# TODO: Trigger scripts / events on cooldown restart. -# TODO: Check locks etc. class GameObjectManager(ObjectManager): GUID_MANAGER = GuidManager() @@ -39,15 +24,13 @@ def __init__(self, **kwargs): self.entry = 0 self.guid = 0 self.gobject_template = None - self.location = None - self.stationary_position = None self.rot0 = 0 self.rot1 = 0 self.rot2 = 0 self.rot3 = 0 - self.summoner = None self.spell_id = 0 # Spell that summoned this object. self.known_players = {} + self.summoner = None self.native_display_id = 0 self.current_display_id = 0 @@ -56,30 +39,50 @@ def __init__(self, **kwargs): self.faction = 0 self.lock = 0 # Unlocked. self.unlocked_by = set() + self.unlock_result = None self.flags = 0 + self.initial_state = 0 self.state = 0 self.update_packet_factory.init_values(self.guid, GameObjectFields) self.time_to_live_timer = 0 self.loot_manager = None # Optional. - self.button_manager = None # Optional. - self.chest_manager = None # Optional. - self.trap_manager = None # Optional. - self.fishing_node_manager = None # Optional. - self.transport_manager = None # Optional. - self.mining_node_manager = None # Optional. - self.goober_manager = None # Optional. - self.ritual_manager = None # Optional. - self.spell_focus_manager = None # Optional. def __hash__(self): return self.guid + # override + def update(self, now): + if now > self.last_tick > 0: + elapsed = now - self.last_tick + + if self.is_active_object(): + # Time to live checks for standalone instances. + if not self._check_time_to_live(elapsed): + return False # Object destroyed. + + # SpellManager update. + self.spell_manager.update(now) + + # Check if this game object should be updated yet or not. + if self.has_pending_updates(): + self.get_map().update_object(self, has_changes=True) + + self.last_tick = now + + def check_cooldown(self, now): + cooldown = self.get_cooldown() + if cooldown > now: + return False + self.set_cooldown(now) + return True # Can use/reset. + def initialize_from_gameobject_template(self, gobject_template): if not gobject_template: return + self.state = self.initial_state self.entry = gobject_template.entry self.gobject_template = gobject_template self.native_display_id = self.gobject_template.display_id @@ -90,60 +93,6 @@ def initialize_from_gameobject_template(self, gobject_template): self.lock = 0 # Unlocked. self.flags = self.gobject_template.flags - # Loot initialization. - if self.gobject_template.type == GameObjectTypes.TYPE_CHEST or \ - self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE: - self.loot_manager = GameObjectLootManager(self) - - # Chest initialization. - if self.gobject_template.type == GameObjectTypes.TYPE_CHEST: - self.chest_manager = ChestManager(self) - - # Mining node initializations. - if self._is_mining_node(): - self.mining_node_manager = MiningNodeManager(self) - - # Button initialization. - if self.gobject_template.type == GameObjectTypes.TYPE_BUTTON: - self.button_manager = ButtonManager(self) - - # Fishing node initialization. - if self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE: - self.fishing_node_manager = FishingNodeManager(self) - - # Transports. - if self.gobject_template.type == GameObjectTypes.TYPE_TRANSPORT: - self.transport_manager = TransportManager(self) - - # Ritual initializations. - if self.gobject_template.type == GameObjectTypes.TYPE_RITUAL: - self.ritual_manager = RitualManager(self) - - # Spell focus objects. - if self.gobject_template.type == GameObjectTypes.TYPE_SPELL_FOCUS: - self.spell_focus_manager = SpellFocusManager(self) - - # Trap initializations. - if self.gobject_template.type == GameObjectTypes.TYPE_TRAP: - self.trap_manager = TrapManager(self) - - # Goober initialization. - if self.gobject_template.type == GameObjectTypes.TYPE_GOOBER: - self.goober_manager = GooberManager(self) - - # Lock initialization for button and door. - if self.gobject_template.type == GameObjectTypes.TYPE_BUTTON or \ - self.gobject_template.type == GameObjectTypes.TYPE_DOOR: - self.lock = self.gobject_template.data1 - - # Lock initialization for quest giver, goober, camera, trap and chest. - if self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER or \ - self.gobject_template.type == GameObjectTypes.TYPE_GOOBER or \ - self.gobject_template.type == GameObjectTypes.TYPE_CAMERA or \ - self.gobject_template.type == GameObjectTypes.TYPE_TRAP or \ - self.gobject_template.type == GameObjectTypes.TYPE_CHEST: - self.lock = gobject_template.data0 - # override def initialize_field_values(self): # Initial field values, after this, fields must be modified by setters or directly writing values to them. @@ -180,78 +129,18 @@ def initialize_field_values(self): self.initialized = True def handle_loot_release(self, player): - # On loot release, always despawn the fishing bobber regardless of it still having loot or not. - if self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE: - self.despawn() + if not self.loot_manager: return - - if self.loot_manager: - # Normal chest. - if not self.mining_node_manager: - # Chest still has loot. - if self.loot_manager.has_loot(): - self.set_ready() - else: # Despawn or destroy. - self.despawn() - # Mining node. - else: - self.mining_node_manager.handle_looted(player) - - def _handle_use_door(self, player): - self.set_active() - - def _handle_use_button(self, player): - self.button_manager.use_button(player) - - def _handle_use_camera(self, player): - cinematic_id = self.gobject_template.data1 - if DbcDatabaseManager.cinematic_sequences_get_by_id(cinematic_id): - data = pack(' 0: - orthogonal_orientation = self.location.o + pi * 0.5 - for x in range(slots): - relative_distance = (self.current_scale * x) - (self.current_scale * (slots - 1) / 2.0) - x_i = self.location.x + relative_distance * cos(orthogonal_orientation) - y_i = self.location.y + relative_distance * sin(orthogonal_orientation) - - player_slot_distance = player.location.distance(Vector(x_i, y_i, player.location.z)) - if player_slot_distance <= lowest_distance: - lowest_distance = player_slot_distance - x_lowest = x_i - y_lowest = y_i - player.teleport(player.map_id, Vector(x_lowest, y_lowest, self.location.z, self.location.o)) - player.set_stand_state(StandState.UNIT_SITTINGCHAIRLOW.value + height) - - # noinspection PyMethodMayBeStatic - def _handle_use_quest_giver(self, player, target): - if target: - player.quest_manager.handle_quest_giver_hello(target, target.guid) - - def _handle_use_chest(self, player): - self.chest_manager.use_chest(player) - - def _handle_fishing_node(self, player): - self.fishing_node_manager.fishing_node_use(player) - - def _handle_use_goober(self, player): - self.goober_manager.goober_use(player) - - def _handle_use_ritual(self, player): - self.ritual_manager.ritual_use(player) + if self.loot_manager.has_loot(): + self.set_ready() + self.set_flag(GameObjectFlags.IN_USE, False) + else: # Despawn or destroy. + self.despawn() # override def is_active_object(self): - return len(self.known_players) > 0 or self.gobject_template.type == GameObjectTypes.TYPE_TRANSPORT + return ((self.is_spawned and self.initialized) and + len(self.known_players) > 0 or self.gobject_template.type == GameObjectTypes.TYPE_TRANSPORT) def apply_spell_damage(self, target, damage, spell_effect, is_periodic=False): # Skip if target is invalid or already dead. @@ -282,56 +171,30 @@ def apply_spell_healing(self, target, value, casting_spell, is_periodic=False): target.receive_healing(value, self) def use(self, player=None, target=None, from_script=False): - if self.gobject_template.type == GameObjectTypes.TYPE_DOOR: - self._handle_use_door(player) - if self.gobject_template.type == GameObjectTypes.TYPE_BUTTON: - self._handle_use_button(player) - elif self.gobject_template.type == GameObjectTypes.TYPE_CAMERA: - self._handle_use_camera(player) - elif self.gobject_template.type == GameObjectTypes.TYPE_CHAIR: - self._handle_use_chair(player) - elif self.gobject_template.type == GameObjectTypes.TYPE_CHEST: - self._handle_use_chest(player) - elif self.gobject_template.type == GameObjectTypes.TYPE_RITUAL: - self._handle_use_ritual(player) - elif self.gobject_template.type == GameObjectTypes.TYPE_GOOBER: - self._handle_use_goober(player) - elif self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER: - self._handle_use_quest_giver(player, target) - elif self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE: - self._handle_fishing_node(player) - if from_script: self.set_active() - else: - # TODO: Do we need separate AI handler for gameobjects? - self.get_map().enqueue_script(self, player, ScriptTypes.SCRIPT_TYPE_GAMEOBJECT, self.spawn_id) # Force surrounding players to refresh this GO interactive state. self.refresh_dynamic_flag() - def set_state(self, state): + def set_state(self, state, force=False): self.state = state - self.set_uint32(GameObjectFields.GAMEOBJECT_STATE, self.state) - - # If not a fishing node, set this go in_use flag. - if not self.fishing_node_manager: - if state == GameObjectStates.GO_STATE_ACTIVE: - self.flags |= GameObjectFlags.IN_USE - self.set_uint32(GameObjectFields.GAMEOBJECT_FLAGS, self.flags) - else: - self.flags &= ~GameObjectFlags.IN_USE - self.set_uint32(GameObjectFields.GAMEOBJECT_FLAGS, self.flags) + self.set_uint32(GameObjectFields.GAMEOBJECT_STATE, self.state, force=force) - def is_transport(self): - return self.gobject_template.type == GameObjectTypes.TYPE_TRANSPORT + def set_flag(self, flag, state): + if state: + self.flags |= flag + else: + self.flags &= ~flag + self.set_uint32(GameObjectFields.GAMEOBJECT_FLAGS, self.flags) def has_flag(self, flag: GameObjectFlags): return self.flags & flag - def set_active(self): + def set_active(self, alternative=False, force=False): if self.state == GameObjectStates.GO_STATE_READY: - self.set_state(GameObjectStates.GO_STATE_ACTIVE) + self.set_state(GameObjectStates.GO_STATE_ACTIVE if not alternative + else GameObjectStates.GO_STATE_ACTIVE_ALTERNATIVE, force=force) return True return False @@ -339,7 +202,7 @@ def refresh_dynamic_flag(self): self.set_uint32(GameObjectFields.GAMEOBJECT_DYN_FLAGS, self.dynamic_flags, force=True) def is_active(self): - return self.state == GameObjectStates.GO_STATE_ACTIVE + return self.state in {GameObjectStates.GO_STATE_ACTIVE, GameObjectStates.GO_STATE_ACTIVE_ALTERNATIVE} def set_ready(self): if self.state != GameObjectStates.GO_STATE_READY: @@ -373,7 +236,11 @@ def send_page_text(self, player_mgr): packet = PacketWriter.get_packet(OpCode.SMSG_GAMEOBJECT_PAGETEXT, pack('= radius: continue - go_object.trap_manager.trigger(who=unit) + if isinstance(go_object, TrapManager) and go_object.is_spawned: + go_object.use(player=unit) + break def cast_spell(self, spell_id, target): spell_template = DbcDatabaseManager.SpellHolder.spell_get_by_id(spell_id) if spell_template: spell_target_mask = spell_template.Targets - casting_spell = self.spell_manager.try_initialize_spell(spell_template, target, + casting_spell = self.spell_manager.try_initialize_spell(spell_template, target if target else self, spell_target_mask, validate=True) self.spell_manager.start_spell_cast(initialized_spell=casting_spell) else: - Logger.warning(f'Invalid spell id for GameObject trap {self.spawn_id}, spell {spell_id}') + Logger.warning(f'Invalid spell id for GameObject {self.get_name()}, Id {self.spawn_id}, spell {spell_id}') def generate_dynamic_field_value(self, requester): go_handled_types = {GameObjectTypes.TYPE_QUESTGIVER, GameObjectTypes.TYPE_GOOBER, GameObjectTypes.TYPE_CHEST} @@ -402,29 +271,29 @@ def generate_dynamic_field_value(self, requester): return 1 return 0 - def _is_mining_node(self): - return self.gobject_template and self.gobject_template.type == GameObjectTypes.TYPE_CHEST and \ - self.gobject_template.data4 != 0 and self.gobject_template.data5 > self.gobject_template.data4 + def has_custom_animation(self): + return self.native_display_id in {2570, 3071, 3072, 3073, 3074, 4392, 4472, 4491, 6785, 6747, 6871} - def get_transport(self): - if self.transport_manager: - return self.transport_manager - return None + def get_auto_close_time(self): + return 0 + + def get_cooldown(self): + return 0 + + def set_cooldown(self, now): + pass + # override def get_fall_time(self): - if self.transport_manager: - return self.transport_manager.get_path_progress() return 0 """ So far this is only needed for GameObjects, client doesn't remove collision for doors sent with active state, so we need to always send them as ready first, and then send the actual state. """ + # Used by DoorManager. def get_door_state_update_bytes(self): - if self.gobject_template.type != GameObjectTypes.TYPE_DOOR or self.state == GameObjectStates.GO_STATE_READY: - return None - # Send real GO state for doors after create packet. - return self.get_single_field_update_bytes(GameObjectFields.GAMEOBJECT_STATE, self.state) + return None def get_dynamic_flag_update_bytes(self, requester): dyn_flag_value = self.generate_dynamic_field_value(requester=requester) @@ -468,38 +337,16 @@ def _check_time_to_live(self, elapsed): return False return True + # override + def respawn(self): + self.initialize_from_gameobject_template(self.gobject_template) + super().respawn() + # override def despawn(self, ttl=0): self.unlocked_by.clear() super().despawn() - # override - def update(self, now): - if now > self.last_tick > 0: - elapsed = now - self.last_tick - - if self.is_spawned and self.initialized: - # Time to live checks for standalone instances. - if not self._check_time_to_live(elapsed): - return # Object destroyed. - - if self.is_active_object(): - if self.trap_manager: - self.trap_manager.update(elapsed) - if self.fishing_node_manager: - self.fishing_node_manager.update(elapsed) - if self.transport_manager and self.transport_manager.has_passengers(): - self.transport_manager.update() - - # SpellManager update. - self.spell_manager.update(now) - - # Check if this game object should be updated yet or not. - if self.has_pending_updates(): - self.get_map().update_object(self, has_changes=True) - - self.last_tick = now - # override def on_cell_change(self): pass @@ -516,9 +363,22 @@ def get_debug_messages(self, requester=None): def get_name(self): return self.gobject_template.name + # override + def get_entry(self): + if self.entry: + return self.entry + if self.gobject_template: + return self.gobject_template.entry + return 0 + + def get_data_field(self, field, data_type): + if not self.gobject_template: + return 0 + return data_type(getattr(self.gobject_template, f'data{field}')) + # override def get_stationary_position(self): - return self.stationary_position if self.stationary_position else self.location + return self.location # override def get_query_details_packet(self): @@ -531,8 +391,6 @@ def get_type_mask(self): # override def get_low_guid(self): - if self.is_transport(): - return self.guid & ~HighGuid.HIGHGUID_TRANSPORT return self.guid & ~HighGuid.HIGHGUID_GAMEOBJECT # override @@ -541,6 +399,4 @@ def get_type_id(self): # override def generate_object_guid(self, low_guid): - if self.is_transport(): - return low_guid | HighGuid.HIGHGUID_TRANSPORT return low_guid | HighGuid.HIGHGUID_GAMEOBJECT diff --git a/game/world/managers/objects/gameobjects/GameObjectSpawn.py b/game/world/managers/objects/gameobjects/GameObjectSpawn.py index 48a55646b..9b87bebee 100644 --- a/game/world/managers/objects/gameobjects/GameObjectSpawn.py +++ b/game/world/managers/objects/gameobjects/GameObjectSpawn.py @@ -46,6 +46,8 @@ def spawn(self, ttl=0, from_pool=False): # New instance for default objects. if self.is_default: self.gameobject_instance = self._generate_gameobject_instance() + if ttl: + self.gameobject_instance.time_to_live_timer = ttl # Triggered objects uses the existent instance. elif not self.gameobject_instance: self.gameobject_instance = self._generate_gameobject_instance(ttl=ttl) @@ -105,7 +107,7 @@ def _generate_gameobject_instance(self, ttl=0): self.respawn_time = randint(self.gameobject_spawn.spawn_spawntimemin, self.gameobject_spawn.spawn_spawntimemax) gameobject_instance = GameObjectBuilder.create(gameobject_template_id, gameobject_location, self.map_id, self.instance_id, - self.gameobject_spawn.spawn_state, + state=self.gameobject_spawn.spawn_state, rot0=self.gameobject_spawn.spawn_rotation0, rot1=self.gameobject_spawn.spawn_rotation1, rot2=self.gameobject_spawn.spawn_rotation2, diff --git a/game/world/managers/objects/gameobjects/managers/ButtonManager.py b/game/world/managers/objects/gameobjects/managers/ButtonManager.py index 183ea229b..c2145006e 100644 --- a/game/world/managers/objects/gameobjects/managers/ButtonManager.py +++ b/game/world/managers/objects/gameobjects/managers/ButtonManager.py @@ -1,14 +1,72 @@ -class ButtonManager: +import time - def __init__(self, button_object): - self.button_object = button_object - self.start_open_state = bool(button_object.gobject_template.data0) - self.lock = button_object.gobject_template.data1 - self.auto_close_secs = button_object.gobject_template.data2 - self.linked_trap = button_object.gobject_template.data3 +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager +from utils.constants.MiscCodes import GameObjectStates +from utils.constants.MiscFlags import GameObjectFlags - def use_button(self, player): - self.button_object.set_active() - if self.linked_trap: - self.button_object.trigger_linked_trap(self.linked_trap, player) +class ButtonManager(GameObjectManager): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.start_open_state = False + self.auto_close_secs = 0 + self.linked_trap = 0 + self.total_cooldown = 0 + + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.start_open_state = self.get_data_field(0, bool) + self.lock = self.get_data_field(1, int) + self.auto_close_secs = self.get_data_field(2, int) # (65536 * seconds) (e.g. open after 5min = 19660800) + self.linked_trap = self.get_data_field(3, int) + + # override + def update(self, now): + if now > self.last_tick > 0: + if self.is_active_object(): + # Check if we need to reset the original button state. + if self.is_active() and super().check_cooldown(now): + self.reset_button_state() + super().update(now) + + # override + def use(self, player=None, target=None, from_script=False): + if not super().check_cooldown(time.time()): + return + + self.switch_button_state(True) + self.set_cooldown(time.time()) + + if not from_script: + self.trigger_script(player) + + if self.linked_trap and player: + self.trigger_linked_trap(self.linked_trap, player) + + super().use(player, target, from_script) + + def reset_button_state(self): + self.switch_button_state(active=False) + self.total_cooldown = 0 + + def switch_button_state(self, active=True, alternative=False): + self.set_flag(GameObjectFlags.IN_USE, active) + + if self.state == GameObjectStates.GO_STATE_READY: # Closed + self.set_active(alternative=alternative) + else: + self.set_ready() + + # override + def get_auto_close_time(self): + return self.auto_close_secs / 0x10000 + + # override + def set_cooldown(self, now): + self.total_cooldown = now + self.get_auto_close_time() + + # override + def get_cooldown(self): + return self.total_cooldown diff --git a/game/world/managers/objects/gameobjects/managers/CameraManager.py b/game/world/managers/objects/gameobjects/managers/CameraManager.py new file mode 100644 index 000000000..d816d7ae0 --- /dev/null +++ b/game/world/managers/objects/gameobjects/managers/CameraManager.py @@ -0,0 +1,34 @@ +from struct import pack + +from database.dbc.DbcDatabaseManager import DbcDatabaseManager +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager +from network.packet.PacketWriter import PacketWriter +from utils.constants.OpCodes import OpCode + + +class CameraManager(GameObjectManager): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.lock = 0 + self.cinematic_id = 0 + self.event_id = 0 + + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.lock = self.get_data_field(0, int) + self.cinematic_id = self.get_data_field(1, int) + self.event_id = self.get_data_field(2, int) + + # override + def use(self, player=None, target=None, from_script=False): + if self.cinematic_id and player: + if DbcDatabaseManager.cinematic_sequences_get_by_id(self.cinematic_id): + packet = PacketWriter.get_packet(OpCode.SMSG_TRIGGER_CINEMATIC, pack(' self.last_tick > 0: + if self.is_active_object(): + # Check if we need to reset the original door state. + if self.is_active() and super().check_cooldown(now): + self.reset_door_state() + super().update(now) + + # override + def use(self, player=None, target=None, from_script=False): + if not super().check_cooldown(time.time()): + return + + self.switch_door_state(True) + self.set_cooldown(time.time()) + + if not from_script: + self.trigger_script(player) + + super().use(player, target, from_script) + + def reset_door_state(self): + self.switch_door_state(active=False) + self.total_cooldown = 0 + + def switch_door_state(self, active=True, alternative=False): + self.set_flag(GameObjectFlags.IN_USE, active) + + if self.state == GameObjectStates.GO_STATE_READY: # Closed + self.set_active(alternative=alternative) + else: + self.set_ready() + + # override + def get_door_state_update_bytes(self): + if self.state == GameObjectStates.GO_STATE_READY: + return None + # Send real GO state for doors after create packet. + return self.get_single_field_update_bytes(GameObjectFields.GAMEOBJECT_STATE, self.state) + + # override + def get_auto_close_time(self): + return self.auto_close_secs / 0x10000 + + # override + def set_cooldown(self, now): + self.total_cooldown = now + self.get_auto_close_time() + + # override + def get_cooldown(self): + return self.total_cooldown diff --git a/game/world/managers/objects/gameobjects/managers/FishingNodeManager.py b/game/world/managers/objects/gameobjects/managers/FishingNodeManager.py index 4b93467b5..15418358d 100644 --- a/game/world/managers/objects/gameobjects/managers/FishingNodeManager.py +++ b/game/world/managers/objects/gameobjects/managers/FishingNodeManager.py @@ -1,45 +1,69 @@ import time from random import randint +from game.world.managers.objects.gameobjects.GameObjectLootManager import GameObjectLootManager +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager from network.packet.PacketWriter import PacketWriter -from utils.constants.MiscCodes import GameObjectStates, ObjectTypeFlags +from utils.constants.MiscCodes import GameObjectStates from utils.constants.OpCodes import OpCode - FISHING_CHANNEL_TIME = 30 # Extracted from SpellDuration.dbc (with ID 9). FISHING_REACTION_TIME = 2.0 # TODO: Reaction time, guessed value. -class FishingNodeManager: - def __init__(self, fishing_node): - self.fishing_node = fishing_node +class FishingNodeManager(GameObjectManager): + def __init__(self, **kwargs): + super().__init__(**kwargs) # TODO: Is this the correct approach for splash generation? self.fishing_timer = randint(1, int(FISHING_CHANNEL_TIME - FISHING_REACTION_TIME)) self.became_active_time = 0 self.hook_result = False self.got_away = False - # Set channel object update field. - if fishing_node.summoner and fishing_node.summoner.get_type_mask() & ObjectTypeFlags.TYPE_UNIT: - fishing_node.summoner.set_channel_object(fishing_node.guid) + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.loot_manager = GameObjectLootManager(self) + + # override + def update(self, now): + if now > self.last_tick > 0: + if self.is_active_object(): + elapsed = now - self.last_tick + self._update(elapsed) + super().update(now) + + def _update(self, elapsed): + if not self.fishing_timer: + return + self.fishing_timer = max(0, self.fishing_timer - elapsed) + if self.fishing_timer: + return + # Became active this tick, activate fishing node. + self.became_active_time = time.time() + self.set_active(force=True) # Since hook is time sensitive, force the update immediately. + self.send_custom_animation(0) - def fishing_node_use(self, player): + # override + def use(self, player=None, target=None, from_script=False): # Generate loot if it's empty. - if not self.fishing_node.loot_manager.has_loot(): - self.fishing_node.loot_manager.generate_loot(player) + if not self.loot_manager.has_loot(): + self.loot_manager.generate_loot(player) - if self.fishing_node.fishing_node_manager.try_hook_attempt(player): - player.send_loot(self.fishing_node.loot_manager) + if player: + if self.try_hook_attempt(player): + player.send_loot(self.loot_manager) + # Remove cast. + player.spell_manager.remove_cast_by_id(self.spell_id) - # Remove cast. - player.spell_manager.remove_cast_by_id(self.fishing_node.spell_id) + super().use(player, target, from_script) def try_hook_attempt(self, player): - if self.fishing_node.state != GameObjectStates.GO_STATE_ACTIVE: + if self.state != GameObjectStates.GO_STATE_ACTIVE: self.hook_result = False elif self.fishing_timer > 0: self.hook_result = False - elif not self.fishing_node.loot_manager.has_loot() or not FishingNodeManager.roll_chance(player): + elif not self.loot_manager.has_loot() or not FishingNodeManager.roll_chance(player): self.got_away = True self.hook_result = False else: @@ -55,6 +79,11 @@ def try_hook_attempt(self, player): return self.hook_result + # override + def handle_loot_release(self, player): + # On loot release, always despawn the fishing bobber regardless of it still having loot or not. + self.despawn() + @staticmethod def roll_chance(player): return player.skill_manager.handle_fishing_attempt_chance() @@ -66,14 +95,3 @@ def _notify_got_away(player): @staticmethod def _notify_not_hooked(player): player.enqueue_packet(PacketWriter.get_packet(OpCode.SMSG_FISH_NOT_HOOKED)) - - def update(self, elapsed): - if not self.fishing_timer: - return - self.fishing_timer = max(0, self.fishing_timer - elapsed) - if self.fishing_timer: - return - # Became active this tick, activate fishing node. - self.fishing_node.send_custom_animation(0) - self.became_active_time = time.time() - self.fishing_node.set_active() diff --git a/game/world/managers/objects/gameobjects/managers/GooberManager.py b/game/world/managers/objects/gameobjects/managers/GooberManager.py index 91ae6f663..6b1c6defb 100644 --- a/game/world/managers/objects/gameobjects/managers/GooberManager.py +++ b/game/world/managers/objects/gameobjects/managers/GooberManager.py @@ -1,18 +1,81 @@ -class GooberManager: - - def __init__(self, goober_object): - self.goober_object = goober_object - self.quest_id = goober_object.gobject_template.data1 - self.has_custom_animation = goober_object.gobject_template.data4 > 0 - self.is_consumable = goober_object.gobject_template.data5 > 0 - self.page_text = goober_object.gobject_template.data7 - - def goober_use(self, player): - if self.has_custom_animation: - self.goober_object.send_custom_animation(0) +import time + +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager +from utils.constants.MiscFlags import GameObjectFlags + + +class GooberManager(GameObjectManager): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.quest_id = 0 + self.event_id = 0 + self.auto_close_secs = 0 + self._has_custom_animation = False + self.is_consumable = False + self.cooldown = 0 + self.page_text = 0 + self.total_cooldown = 0 + + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.lock = self.get_data_field(0, int) + self.quest_id = self.get_data_field(1, bool) + self.event_id = self.get_data_field(2, int) + self.auto_close_secs = self.get_data_field(3, int) + self._has_custom_animation = self.get_data_field(4, bool) + self.is_consumable = self.get_data_field(5, bool) + self.cooldown = self.get_data_field(6, int) + self.page_text = self.get_data_field(7, int) + self.total_cooldown = time.time() + self.cooldown + + # override + def use(self, player=None, target=None, from_script=False): + if not super().check_cooldown(time.time()): + return + if self.is_consumable: - self.goober_object.despawn() - if self.page_text: - self.goober_object.send_page_text(player) - if self.quest_id: - player.quest_manager.handle_goober_use(self.goober_object, self.quest_id) + self.despawn() + + if player: + if self.page_text: + self.send_page_text(player) + if self.quest_id: + player.quest_manager.handle_goober_use(self, self.quest_id) + if not from_script: + self.trigger_script(player) + + self.set_flag(GameObjectFlags.IN_USE, state=True) + + time_to_restore = self.get_auto_close_time() + + if self.has_custom_animation() or self._has_custom_animation and time_to_restore: + self.send_custom_animation(0) + else: + self.set_active() + + self.cooldown = time_to_restore + time.time() + + if self.spell_id: + self.cast_spell(self.spell_id, player) + + super().use(player, target, from_script) + + # override + def has_custom_animation(self): + if self._has_custom_animation: + return True + return super().has_custom_animation() + + # override + def get_auto_close_time(self): + return self.auto_close_secs / 0x10000 + + # override + def get_cooldown(self): + return self.total_cooldown + + # override + def set_cooldown(self, now): + self.total_cooldown = now + self.cooldown diff --git a/game/world/managers/objects/gameobjects/managers/MiningNodeManager.py b/game/world/managers/objects/gameobjects/managers/MiningNodeManager.py index d09a57ce1..2a0a65df2 100644 --- a/game/world/managers/objects/gameobjects/managers/MiningNodeManager.py +++ b/game/world/managers/objects/gameobjects/managers/MiningNodeManager.py @@ -1,16 +1,47 @@ import math import random +from game.world.managers.objects.gameobjects.GameObjectLootManager import GameObjectLootManager +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager +from utils.constants.MiscFlags import GameObjectFlags +from utils.constants.UnitCodes import UnitFlags -class MiningNodeManager: - def __init__(self, mining_node): - self.mining_node = mining_node - self.min_restock = mining_node.gobject_template.data4 - self.max_restock = mining_node.gobject_template.data5 + +class MiningNodeManager(GameObjectManager): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.min_restock = 0 + self.max_restock = 0 + self.level_min = 0 self.attempts = 0 - # TODO: Need to handle chance also on spell effect. - def handle_looted(self, player): + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.lock = self.get_data_field(0, int) + self.min_restock = self.get_data_field(4, int) + self.max_restock = self.get_data_field(5, int) + self.level_min = self.get_data_field(9, int) + self.loot_manager = GameObjectLootManager(self) + + # override + def use(self, player=None, target=None, from_script=False): + # Activate chest open animation, while active, it won't let any other player loot. + self.set_active() + self.set_flag(GameObjectFlags.IN_USE, True) + + if player: + # Player kneel loot. + player.set_unit_flag(UnitFlags.UNIT_FLAG_LOOTING, active=True) + + # Generate loot if it's empty. + if not self.loot_manager.has_loot(): + self.loot_manager.generate_loot(player) + + player.send_loot(self.loot_manager) + + # override + def handle_loot_release(self, player): self.attempts += 1 amount_rate = 1.0 min_amount = self.min_restock * amount_rate @@ -18,27 +49,30 @@ def handle_looted(self, player): # Max attempts, despawn. if self.attempts >= max_amount: - self.mining_node.despawn() + self.despawn() return # 100% chance until min uses. if self.attempts < min_amount: - self.mining_node.set_ready() - self.mining_node.loot_manager.generate_loot(player) + self.set_ready() + self.set_flag(GameObjectFlags.IN_USE, False) + self.loot_manager.generate_loot(player) return chance_rate = 1.0 - # TODO: need to override required_value from Locks.dbc, if available. required_value = 175 - skill_total = player.skill_manager.get_total_skill_value(11) / (required_value + 25) + if self.unlock_result: + required_value = self.unlock_result.required_skill_value + skill_total = player.skill_manager.get_total_skill_value(186) / (required_value + 25) chance = math.pow(0.8 * chance_rate, 4 * (1 / self.max_restock) * self.attempts) succeed_roll = 100.0 * chance + skill_total > random.uniform(0.0, 99.9999999) # Failed chance roll, despawn. if not succeed_roll: - self.mining_node.despawn() + self.despawn() return # Node still alive, regenerate loot. - self.mining_node.set_ready() - self.mining_node.loot_manager.generate_loot(player) + self.set_ready() + self.set_flag(GameObjectFlags.IN_USE, False) + self.loot_manager.generate_loot(player) diff --git a/game/world/managers/objects/gameobjects/managers/QuestGiverManager.py b/game/world/managers/objects/gameobjects/managers/QuestGiverManager.py new file mode 100644 index 000000000..96f0311ed --- /dev/null +++ b/game/world/managers/objects/gameobjects/managers/QuestGiverManager.py @@ -0,0 +1,19 @@ +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager + + +class QuestGiverManager(GameObjectManager): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.lock = self.get_data_field(0, int) + + # override + def use(self, player=None, target=None, from_script=False): + if target and player: + player.quest_manager.handle_quest_giver_hello(target, target.guid) + + super().use(player, target, from_script) diff --git a/game/world/managers/objects/gameobjects/managers/RitualManager.py b/game/world/managers/objects/gameobjects/managers/RitualManager.py index 1b09805cd..eb8dd7f2c 100644 --- a/game/world/managers/objects/gameobjects/managers/RitualManager.py +++ b/game/world/managers/objects/gameobjects/managers/RitualManager.py @@ -1,83 +1,84 @@ from database.dbc.DbcDatabaseManager import DbcDatabaseManager +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager from utils.Logger import Logger -from utils.constants.MiscCodes import ObjectTypeIds, ObjectTypeFlags from utils.constants.SpellCodes import SpellTargetMask, SpellCheckCastResult -class RitualManager: - def __init__(self, ritual_object): - self.ritual_object = ritual_object - self.required_participants = ritual_object.gobject_template.data0 - 1 # -1 to include caster. - self.ritual_summon_spell_id = ritual_object.gobject_template.data1 - self.ritual_channel_spell_id = ritual_object.gobject_template.data2 - self.persistent = ritual_object.gobject_template.data3 > 0 - self.caster_target_spell = ritual_object.gobject_template.data4 - self.caster_target_spell_targets = ritual_object.gobject_template.data5 > 0 - self.casters_grouped = ritual_object.gobject_template.data6 > 0 +class RitualManager(GameObjectManager): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.required_participants = 0 + self.summon_spell_id = 0 + self.channel_spell_id = 0 + self.persistent = False + self.caster_target_spell = 0 + self.caster_target_spell_targets = False + self.casters_grouped = False self.ritual_participants = [] - # Set channel object update field. - if ritual_object.summoner and ritual_object.summoner.get_type_mask() & ObjectTypeFlags.TYPE_UNIT: - ritual_object.summoner.set_channel_object(ritual_object.guid) + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.required_participants = self.get_data_field(0, int) - 1 # -1 to include caster. + self.summon_spell_id = self.get_data_field(1, int) + self.channel_spell_id = self.get_data_field(2, int) + self.persistent = self.get_data_field(3, bool) + self.caster_target_spell = self.get_data_field(4, int) + self.caster_target_spell_targets = self.get_data_field(5, bool) + self.casters_grouped = self.get_data_field(6, bool) - def ritual_use(self, player_mgr): - # Ritual should have a summoner. - if not self.ritual_object.summoner: - Logger.warning(f'Player {player_mgr.get_name()} tried to use Ritual with no summoner set.') - player_mgr.spell_manager.send_cast_result(self.ritual_summon_spell_id, - SpellCheckCastResult.SPELL_FAILED_BAD_TARGETS) - return - - # Grab the ritual summoner. - summoner = self.ritual_object.summoner - - # Group check for players. - if summoner.get_type_id() == ObjectTypeIds.ID_PLAYER and self.casters_grouped: - if not summoner.group_manager or not summoner.group_manager.is_party_member(player_mgr.guid): - player_mgr.spell_manager.send_cast_result(self.ritual_summon_spell_id, - SpellCheckCastResult.SPELL_FAILED_TARGET_NOT_IN_PARTY) + # override + def use(self, player=None, target=None, from_script=False): + if player: + # Ritual should have a summoner. + if not self.summoner: + Logger.warning(f'Player {player.get_name()} tried to use Ritual with no summoner set.') + player.spell_manager.send_cast_result(self.summon_spell_id, SpellCheckCastResult.SPELL_FAILED_BAD_TARGETS) return - if player_mgr is summoner or player_mgr in self.ritual_participants: - return # No action needed for this player. + if player is self.summoner or player in self.ritual_participants: + return # No action needed for this player. - # Make the player channel for summoning. - channel_spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(self.ritual_channel_spell_id) - spell = player_mgr.spell_manager.try_initialize_spell(channel_spell_entry, self, SpellTargetMask.GAMEOBJECT, + # Make the player channel for summoning. + channel_spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(self.channel_spell_id) + spell = player.spell_manager.try_initialize_spell(channel_spell_entry, self, SpellTargetMask.GAMEOBJECT, validate=False) - # Note: these triggered casts will skip the actual effects of the summoning spell, only starting the channel. - player_mgr.spell_manager.remove_colliding_casts(spell) - player_mgr.spell_manager.casting_spells.append(spell) - player_mgr.spell_manager.handle_channel_start(spell) - player_mgr.set_channel_object(self.ritual_object.guid) - self.ritual_participants.append(player_mgr) + # These triggered casts will skip the actual effects of the summoning spell, only starting the channel. + player.spell_manager.remove_colliding_casts(spell) + player.spell_manager.casting_spells.append(spell) + player.spell_manager.handle_channel_start(spell) + player.set_channel_object(self.guid) + self.ritual_participants.append(player) + + # Check if the ritual can be completed with the current participants. + if len(self.ritual_participants) >= self.required_participants: + # Cast the finishing spell. + spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(self.summon_spell_id) + spell_cast = self.summoner.spell_manager.try_initialize_spell(spell_entry, self.summoner, + SpellTargetMask.SELF, + triggered=True, validate=False) + if spell_cast: + self.summoner.spell_manager.start_spell_cast(initialized_spell=spell_cast) + else: + # Interrupt ritual channel if summon fails. + self.summoner.spell_manager.remove_cast_by_id(self.channel_spell_id) - # Check if the ritual can be completed with the current participants. - if len(self.ritual_participants) >= self.required_participants: - # Cast the finishing spell. - spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(self.ritual_summon_spell_id) - spell_cast = summoner.spell_manager.try_initialize_spell(spell_entry, summoner, SpellTargetMask.SELF, - triggered=True, validate=False) - if spell_cast: - summoner.spell_manager.start_spell_cast(initialized_spell=spell_cast) - else: - # Interrupt ritual channel if summon fails. - summoner.spell_manager.remove_cast_by_id(self.ritual_channel_spell_id) + super().use(player, target, from_script) def channel_end(self, caster): # If the ritual caster interrupts channeling, interrupt others and remove the portal. - if caster is self.ritual_object.summoner: - self.ritual_object.get_map().remove_object(self.ritual_object) + if caster is self.summoner: + self.get_map().remove_object(self) for player in self.ritual_participants: # Note that this call will lead to _handle_summoning_channel_end() calls from the participants. - player.spell_manager.remove_cast_by_id(self.ritual_channel_spell_id) + player.spell_manager.remove_cast_by_id(self.channel_spell_id) # If a participant interrupts their channeling, remove from participants and interrupt summoning if necessary. elif caster in self.ritual_participants: self.ritual_participants.remove(caster) caster.flush_channel_fields() if not self.meets_participants(): - self.ritual_object.summoner.spell_manager.remove_cast_by_id(self.ritual_summon_spell_id) + self.summoner.spell_manager.remove_cast_by_id(self.summon_spell_id) def meets_participants(self): return len(self.ritual_participants) >= self.required_participants diff --git a/game/world/managers/objects/gameobjects/managers/SpellFocusManager.py b/game/world/managers/objects/gameobjects/managers/SpellFocusManager.py index ef20b154e..9587fe7af 100644 --- a/game/world/managers/objects/gameobjects/managers/SpellFocusManager.py +++ b/game/world/managers/objects/gameobjects/managers/SpellFocusManager.py @@ -1,14 +1,22 @@ -from database.dbc.DbcDatabaseManager import DbcDatabaseManager +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager -class SpellFocusManager: - def __init__(self, gameobject): - self.gameobject = gameobject - self.spell_focus_object = DbcDatabaseManager.spell_get_focus_by_id(gameobject.gobject_template.data0) - self.radius = gameobject.gobject_template.data1 / 2.0 - self.linked_trap = gameobject.gobject_template.data2 +class SpellFocusManager(GameObjectManager): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.spell_focus_type = 0 + self.radius = 0 + self.linked_trap = 0 - def use_spell_focus(self, who): - if self.linked_trap: - self.gameobject.trigger_linked_trap(self.linked_trap, who, self.radius) - return True + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.spell_focus_type = self.get_data_field(0, int) + self.radius = self.get_data_field(1, float) / 2.0 + self.linked_trap = self.get_data_field(2, int) + + # override + def use(self, player=None, target=None, from_script=False): + if player and self.linked_trap: + self.trigger_linked_trap(self.linked_trap, player, self.radius) + super().use(player, target, from_script) diff --git a/game/world/managers/objects/gameobjects/managers/TransportManager.py b/game/world/managers/objects/gameobjects/managers/TransportManager.py index a07c3aa30..2765b8e91 100644 --- a/game/world/managers/objects/gameobjects/managers/TransportManager.py +++ b/game/world/managers/objects/gameobjects/managers/TransportManager.py @@ -1,31 +1,48 @@ import math + from database.dbc.DbcDatabaseManager import DbcDatabaseManager from database.dbc.DbcModels import TransportAnimation from game.world import WorldManager from bisect import bisect_left from game.world.managers.abstractions.Vector import Vector +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager from utils.ConfigManager import config -from utils.constants.MiscCodes import GameObjectStates +from utils.constants.MiscCodes import GameObjectStates, HighGuid # TODO: Players automatically desync to other player viewers when inside transports. # this seems to be all client related since we've tried many changes based on other cores and nothing seems to work. # From 0.5.4 patch notes. 'fixed problems with elevators.' # From 0.7.1 patch notes. 'fixed multiple crashes related to both players and pets on elevators' -class TransportManager: - def __init__(self, owner): - self.owner = owner - self.entry = owner.gobject_template.entry +class TransportManager(GameObjectManager): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.passengers = {} - self.current_anim_position = owner.location + self.current_anim_position = self.location self.path_progress = 0.0 self.total_time = 0.0 self.current_segment = 0 self.path_nodes: dict[int, TransportAnimation] = {} self.load_path_nodes() + self.stationary_position = self.location.copy() + self.auto_close_secs = 0 + + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.auto_close_secs = self.get_data_field(3, int) + + # override + def update(self, now): + if now > self.last_tick > 0: + if self.is_active_object() and self.has_passengers(): + self._calculate_progress() + self._update_passengers() + super().update(now) def load_path_nodes(self): - for node in DbcDatabaseManager.TransportAnimationHolder.animations_by_entry(self.entry): + for node in DbcDatabaseManager.TransportAnimationHolder.animations_by_entry(self.get_entry()): self.path_nodes[node.TimeIndex] = node if self.total_time < node.TimeIndex: self.total_time = node.TimeIndex @@ -44,19 +61,42 @@ def get_next_node(self, time): return lower_bound return None - def get_path_progress(self): - return self.update() + # override + def get_fall_time(self): + self._calculate_progress() + return int(self.path_progress) def has_passengers(self): return len(self.passengers) > 0 - def update(self): + def calculate_passenger_position(self, player_mgr): + in_x = player_mgr.transport_location.x + in_y = player_mgr.transport_location.y + in_z = player_mgr.transport_location.z + in_o = player_mgr.transport_location.o + + trans_x = self.location.x + trans_y = self.location.y + trans_z = self.location.z + trans_o = self.location.o + + x = trans_x + in_x * math.cos(trans_o) - in_y * math.sin(trans_o) + y = trans_y + in_y * math.cos(trans_o) + in_x * math.sin(trans_o) + z = trans_z + in_z + o = TransportManager.normalize_orientation(trans_o + in_o) + + player_mgr.location.x = x + player_mgr.location.y = y + player_mgr.location.z = z + player_mgr.location.o = o + + def _calculate_progress(self): self.path_progress = self._get_time() next_node = self.get_next_node(self.path_progress) prev_node = self.get_previous_node(self.path_progress) # No progress. if not next_node or not prev_node: - return int(self.path_progress) + return self.current_segment = prev_node.TimeIndex prev_pos = Vector(prev_node.X, prev_node.Y, prev_node.Z) next_pos = Vector(next_node.X, next_node.Y, next_node.Z) @@ -74,47 +114,18 @@ def update(self): location = Vector(time_elapsed * velocity_x, time_elapsed * velocity_y, time_elapsed * velocity_z) location += prev_pos - self.current_anim_position = self.owner.get_stationary_position() + location + self.current_anim_position = self.get_stationary_position() + location if config.Server.Settings.debug_transport: self._debug_position(self.current_anim_position) - self.owner.location.z = self.current_anim_position.z - - self._update_passengers() - - return int(self.path_progress) - - def calculate_passenger_position(self, player_mgr): - in_x = player_mgr.transport_location.x - in_y = player_mgr.transport_location.y - in_z = player_mgr.transport_location.z - in_o = player_mgr.transport_location.o - - trans_x = self.owner.location.x - trans_y = self.owner.location.y - trans_z = self.owner.location.z - trans_o = self.owner.location.o - - x = trans_x + in_x * math.cos(trans_o) - in_y * math.sin(trans_o) - y = trans_y + in_y * math.cos(trans_o) + in_x * math.sin(trans_o) - z = trans_z + in_z - o = TransportManager.normalize_orientation(trans_o + in_o) - - player_mgr.location.x = x - player_mgr.location.y = y - player_mgr.location.z = z - player_mgr.location.o = o + self.location.z = self.current_anim_position.z def _update_passengers(self): for unit in list(self.passengers.values()): self.calculate_passenger_position(unit) unit.movement_info.send_surrounding_update() - @staticmethod - def normalize_orientation(o): - return math.fmod(o, 2.0*math.pi) - def add_passenger(self, unit): self.passengers[unit.guid] = unit @@ -129,11 +140,29 @@ def update_passengers(self): def _debug_position(self, location): from game.world.managers.objects.gameobjects.GameObjectBuilder import GameObjectBuilder - gameobject = GameObjectBuilder.create(176557, location, self.owner.map_id, self.owner.instance_id, - GameObjectStates.GO_STATE_READY, - summoner=self.owner, - ttl=1) - self.owner.get_map().spawn_object(world_object_instance=gameobject) + gameobject = GameObjectBuilder.create(176557, location, self.map_id, self.instance_id, + GameObjectStates.GO_STATE_READY, summoner=self, ttl=1) + self.get_map().spawn_object(world_object_instance=gameobject) + + # override + def get_auto_close_time(self): + return self.auto_close_secs / 0x10000 def _get_time(self): return int(WorldManager.get_seconds_since_startup() * 1000) % self.total_time + + # override + def get_low_guid(self): + return self.guid & ~HighGuid.HIGHGUID_TRANSPORT + + # override + def get_stationary_position(self): + return self.stationary_position + + # override + def generate_object_guid(self, low_guid): + return low_guid | HighGuid.HIGHGUID_TRANSPORT + + @staticmethod + def normalize_orientation(o): + return math.fmod(o, 2.0*math.pi) diff --git a/game/world/managers/objects/gameobjects/managers/TrapManager.py b/game/world/managers/objects/gameobjects/managers/TrapManager.py index 8b25ee220..c42158f07 100644 --- a/game/world/managers/objects/gameobjects/managers/TrapManager.py +++ b/game/world/managers/objects/gameobjects/managers/TrapManager.py @@ -1,59 +1,97 @@ -class TrapManager: +import time + +from game.world.managers.objects.gameobjects.GameObjectManager import GameObjectManager + + +class TrapManager(GameObjectManager): TRIGGERED_BY_CREATURES = { 3355 # zzOldSnare Trap Effect (Snare Trap 1499) } - def __init__(self, trap_object): - self.trap_object = trap_object - self.lock = trap_object.gobject_template.data0 - self.level_min = trap_object.gobject_template.data1 - self.radius = trap_object.gobject_template.data2 / 2.0 - self.spell_id = trap_object.gobject_template.data3 + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.level_min = 0 + self.radius = 0 # Can only be 0 (Infinite Trigger) or 1 (Should despawn after trigger). - self.charges = trap_object.gobject_template.data4 - self.cooldown = 1 if not trap_object.gobject_template.data5 else trap_object.gobject_template.data5 - self.start_delay = trap_object.gobject_template.data7 - self.remaining_cooldown = self.start_delay + self.charges = 0 + self.cooldown = 0 + self.auto_close_secs = 0 + self.start_delay = 0 + self.total_cooldown = 0 - def is_ready(self): - return self.remaining_cooldown == 0 + # override + def initialize_from_gameobject_template(self, gobject_template): + super().initialize_from_gameobject_template(gobject_template) + self.lock = self.get_data_field(0, int) + self.level_min = self.get_data_field(1, int) + self.radius = self.get_data_field(2, float) / 2.0 + self.spell_id = self.get_data_field(3, int) + # Can only be 0 (Infinite Trigger) or 1 (Should despawn after trigger). + self.charges = self.get_data_field(4, int) + self.cooldown = self.get_data_field(5, int) + self.auto_close_secs = self.get_data_field(6, int) + self.start_delay = self.get_data_field(7, int) + self.total_cooldown = time.time() + self.cooldown + self.start_delay - def _is_triggered_by_proximity(self): - return self.radius > 0 + # override + def update(self, now): + if now > self.last_tick > 0: + if self.is_active_object(): + self._update() + super().update(now) - def update(self, elapsed): + def _update(self): if not self._is_triggered_by_proximity(): return - if not self.is_ready(): - # Infinite trigger, set go as ready until triggered. - if not self.charges: - self.trap_object.set_ready() - self.remaining_cooldown = max(0, self.remaining_cooldown - elapsed) - return + # Infinite trigger, set go as ready until triggered. + if not self.charges: + self.set_ready() # If the trap should be triggered by creatures, search for them along with players. if self.spell_id in TrapManager.TRIGGERED_BY_CREATURES: - surrounding_creatures, surrounding_players = self.trap_object.get_map().get_surrounding_units_by_location( - self.trap_object.location, self.trap_object.map_id, self.trap_object.instance_id, - self.radius, include_players=True) - surrounding_units = surrounding_creatures | surrounding_players + creatures, players = self.get_map().get_surrounding_units_by_location(self.location, self.map_id, + self.instance_id, self.radius, + include_players=True) + units = creatures | players else: # This trap can only be triggered by players. - surrounding_units = self.trap_object.get_map().get_surrounding_players_by_location( - self.trap_object.location, self.trap_object.map_id, self.trap_object.instance_id, self.radius) + units = self.get_map().get_surrounding_players_by_location(self.location, self.map_id, self.instance_id, + self.radius) - for unit in surrounding_units.values(): + for unit in units.values(): # Keep looping until we find a valid unit. - if not self.trap_object.can_attack_target(unit): + if not self.can_attack_target(unit): continue - self.trigger(unit) + self.use(player=unit) break - self.remaining_cooldown = self.cooldown + # override + def use(self, player=None, target=None, from_script=False): + if not super().check_cooldown(time.time()): + return - def trigger(self, who): - self.trap_object.set_active() - self.trap_object.cast_spell(self.spell_id, who) + self.set_active() + self.cast_spell(self.spell_id, player) + if self.has_custom_animation(): + self.send_custom_animation(0) if self.charges == 1: - self.trap_object.despawn() + self.despawn() + return + + super().use(player, target, from_script) + + # override + def get_auto_close_time(self): + return self.auto_close_secs / 0x10000 + + # override + def get_cooldown(self): + return self.total_cooldown + + # override + def set_cooldown(self, now): + self.total_cooldown = now + self.cooldown + self.start_delay + + def _is_triggered_by_proximity(self): + return self.radius > 0 and self.cooldown diff --git a/game/world/managers/objects/item/ItemManager.py b/game/world/managers/objects/item/ItemManager.py index 3799875ea..4951e0591 100644 --- a/game/world/managers/objects/item/ItemManager.py +++ b/game/world/managers/objects/item/ItemManager.py @@ -532,6 +532,14 @@ def get_owner_unit(self): def get_name(self): return self.item_template.name if self.item_template else 'Backpack' if self.is_backpack else 'None' + # override + def get_entry(self): + if self.entry: + return self.entry + if self.item_template: + return self.item_template.entry + return 0 + def get_query_details_packet(self): data = self.query_details_data() return PacketWriter.get_packet(OpCode.SMSG_ITEM_QUERY_SINGLE_RESPONSE, data) diff --git a/game/world/managers/objects/script/Script.py b/game/world/managers/objects/script/Script.py index 630a7eb36..c22e89993 100644 --- a/game/world/managers/objects/script/Script.py +++ b/game/world/managers/objects/script/Script.py @@ -54,10 +54,10 @@ def update(self, now): script_command.target = target # Check if source or target are currently in inactive cells, if so, make their cells become active. - if source and not source.is_active_object(): - source.get_map().update_object(source) - if target and target != source and not target.is_active_object(): - target.get_map().update_object(target) + if source and not source.get_map().is_active_cell(source.current_cell): + source.get_map().activate_cell_by_world_object(source) + if target and target != source and not target.get_map().is_active_cell(target.current_cell): + target.get_map().activate_cell_by_world_object(target) # Condition is not met, skip. if not ConditionChecker.validate(script_command.condition_id, source=self.source, target=self.target): diff --git a/game/world/managers/objects/script/ScriptHandler.py b/game/world/managers/objects/script/ScriptHandler.py index 097f41776..f8d0d8fba 100644 --- a/game/world/managers/objects/script/ScriptHandler.py +++ b/game/world/managers/objects/script/ScriptHandler.py @@ -5,6 +5,8 @@ from database.world.WorldDatabaseManager import WorldDatabaseManager from database.world.WorldModels import CreatureGroup from game.world.managers.abstractions.Vector import Vector +from game.world.managers.objects.gameobjects.managers.ButtonManager import ButtonManager +from game.world.managers.objects.gameobjects.managers.DoorManager import DoorManager from game.world.managers.objects.script.ConditionChecker import ConditionChecker from game.world.managers.objects.script.Script import Script from game.world.managers.objects.script.ScriptHelpers import ScriptHelpers @@ -432,6 +434,7 @@ def handle_script_command_respawn_gameobject(command): if not go_spawn: Logger.warning(f'ScriptHandler: No gameobject {command.datalong} found, {command.get_info()}.') return command.should_abort() + go_spawn.spawn(ttl=command.datalong2) return False @@ -855,7 +858,6 @@ def handle_script_command_set_run(command): flag_changed = ((run_enabled and command.source.movement_flags & MoveFlags.MOVEFLAG_WALK) or (not run_enabled and not command.source.movement_flags & MoveFlags.MOVEFLAG_WALK)) if flag_changed: - Logger.script(f"{command.source.get_name()} is now {'Running' if run_enabled else 'Walking'}.") command.source.set_move_flag(MoveFlags.MOVEFLAG_WALK, active=not run_enabled) command.source.movement_manager.set_speed_dirty() @@ -1629,7 +1631,11 @@ def handle_script_command_reset_door_or_button(command): Logger.warning(f'ScriptHandler: Invalid object type (needs to be gameobject) for {command.get_info()}') return command.should_abort() - command.source.gameobject_instance.set_ready() + if isinstance(command.source.gameobject_instance, DoorManager): + command.source.gameobject_instance.reset_door_state() + elif isinstance(command.source.gameobject_instance, ButtonManager): + command.source.gameobject_instance.reset_button_state() + return False @staticmethod diff --git a/game/world/managers/objects/spell/EffectTargets.py b/game/world/managers/objects/spell/EffectTargets.py index f8731addd..403fe084f 100644 --- a/game/world/managers/objects/spell/EffectTargets.py +++ b/game/world/managers/objects/spell/EffectTargets.py @@ -370,7 +370,8 @@ def resolve_all_enemy_in_area_instant(casting_spell, target_effect): casting_spell.spell_impact_timestamps[enemy.guid] = -1 if len(enemies) == 0 and len(target_effect.targets.resolved_targets_a) > 0: - target_effect.targets.resolved_targets_a = [] # As this target specifies on A in some cases, clear out A if no targets exist. + # As this target specifies on A in some cases, clear out A if no targets exist. + target_effect.targets.resolved_targets_a = [] return enemies @staticmethod @@ -378,6 +379,9 @@ def resolve_table_coordinates(casting_spell, target_effect): target_position = WorldDatabaseManager.spell_target_position_get_by_spell(casting_spell.spell_entry.ID) if not target_position: Logger.warning(f'Unimplemented target spell position for spell {casting_spell.spell_entry.ID}.') + # Not available in db tables, return initial target if available. + if casting_spell.initial_target and casting_spell.initial_target_is_terrain(): + return [casting_spell.initial_target] return [] return target_position.target_map, Vector(target_position.target_position_x, diff --git a/game/world/managers/objects/spell/SpellEffectHandler.py b/game/world/managers/objects/spell/SpellEffectHandler.py index 8224a71c0..83b24637e 100644 --- a/game/world/managers/objects/spell/SpellEffectHandler.py +++ b/game/world/managers/objects/spell/SpellEffectHandler.py @@ -46,6 +46,14 @@ def apply_effect(casting_spell, effect, caster, target): SPELL_EFFECTS[effect.effect_type](casting_spell, effect, caster, target) + @staticmethod + def handle_activate_object(casting_spell, effect, caster, target): + if not target.get_type_mask() & ObjectTypeFlags.TYPE_GAMEOBJECT: + return + # Only two spells, Summon Voidwalker and Summon Succubus (Need summoning circle). + # Both with same action, 'AnimateCustom0'. + target.send_custom_animation(0) + @staticmethod def handle_school_damage(casting_spell, effect, caster, target): if not target.get_type_mask() & ObjectTypeFlags.TYPE_UNIT: @@ -654,28 +662,17 @@ def handle_summon_wild(casting_spell, effect, caster, target): for count in range(amount): if casting_spell.spell_target_mask & SpellTargetMask.DEST_LOCATION: if count == 0: - px = target.x - py = target.y - pz = target.z + location = target else: location = caster.location.get_random_point_in_radius(radius, caster.map_id) - px = location.x - py = location.y - pz = location.z else: if radius > 0.0: location = caster.location.get_random_point_in_radius(radius, caster.map_id) - px = location.x - py = location.y - pz = location.z else: location = target if isinstance(target, Vector) else target.location - px = location.x - py = location.y - pz = location.z # Spawn the summoned unit. - creature_manager = CreatureBuilder.create(creature_entry, Vector(px, py, pz), caster.map_id, + creature_manager = CreatureBuilder.create(creature_entry, location, caster.map_id, caster.instance_id, summoner=caster, faction=caster.faction, ttl=duration, spell_id=casting_spell.spell_entry.ID, @@ -971,6 +968,7 @@ def handle_send_event(casting_spell, effect, caster, target): SPELL_EFFECTS = { + SpellEffects.SPELL_EFFECT_ACTIVATE_OBJECT: SpellEffectHandler.handle_activate_object, SpellEffects.SPELL_EFFECT_SCHOOL_DAMAGE: SpellEffectHandler.handle_school_damage, SpellEffects.SPELL_EFFECT_HEAL: SpellEffectHandler.handle_heal, SpellEffects.SPELL_EFFECT_HEAL_MAX_HEALTH: SpellEffectHandler.handle_heal_max_health, diff --git a/game/world/managers/objects/spell/SpellManager.py b/game/world/managers/objects/spell/SpellManager.py index ab7c7e456..31cc362c8 100644 --- a/game/world/managers/objects/spell/SpellManager.py +++ b/game/world/managers/objects/spell/SpellManager.py @@ -11,6 +11,9 @@ from game.world.WorldSessionStateHandler import WorldSessionStateHandler from game.world.managers.abstractions.Vector import Vector from game.world.managers.objects.ObjectManager import ObjectManager +from game.world.managers.objects.gameobjects.managers.FishingNodeManager import FishingNodeManager +from game.world.managers.objects.gameobjects.managers.RitualManager import RitualManager +from game.world.managers.objects.gameobjects.managers.SpellFocusManager import SpellFocusManager from game.world.managers.objects.item.ItemManager import ItemManager from game.world.managers.objects.locks.LockManager import LockManager from game.world.managers.objects.spell import ExtendedSpellData @@ -1168,8 +1171,8 @@ def validate_cast(self, casting_spell) -> bool: self.send_cast_result(casting_spell, SpellCheckCastResult.SPELL_FAILED_REQUIRES_SPELL_FOCUS, spell_focus_type) return False - if spell_focus_object[0].spell_focus_manager: - spell_focus_object[0].spell_focus_manager.use_spell_focus(self.caster) + if isinstance(spell_focus_object[0], SpellFocusManager): + spell_focus_object[0].use_spell_focus(self.caster) # Target validation. validation_target = casting_spell.initial_target @@ -1470,6 +1473,10 @@ def validate_cast(self, casting_spell) -> bool: # Skill check only on initial validation. unlock_result = LockManager.can_open_lock(self.caster, open_lock_effect.misc_value, validation_target.lock, cast_item=casting_spell.source_item, bonus_points=bonus_skill) + + if casting_spell.initial_target_is_gameobject(): + casting_spell.initial_target.unlock_result = unlock_result + unlock_result = unlock_result.result else: # Include failure chance on cast. @@ -1477,6 +1484,7 @@ def validate_cast(self, casting_spell) -> bool: validation_target.lock, used_item=casting_spell.source_item, bonus_skill=bonus_skill) + if unlock_result != SpellCheckCastResult.SPELL_NO_ERROR: self.send_cast_result(casting_spell, unlock_result) return False @@ -1541,22 +1549,20 @@ def _validate_summon_cast(self, casting_spell) -> bool: def _handle_fishing_node_end(self): if not self.caster.channel_object: return - fishing_node_object = self.caster.get_map().get_surrounding_gameobject_by_guid(self.caster, self.caster.channel_object) - if not fishing_node_object or fishing_node_object.gobject_template.type != GameObjectTypes.TYPE_FISHINGNODE: - return - # If this was an interrupt or miss hook, remove the bobber. - # Else, it will be removed upon CMSG_LOOT_RELEASE. - if not fishing_node_object.fishing_node_manager.hook_result: - self.caster.get_map().remove_object(fishing_node_object) + fishing_node = self.caster.get_map().get_surrounding_gameobject_by_guid(self.caster, self.caster.channel_object) + if isinstance(fishing_node, FishingNodeManager): + # If this was an interrupt or miss hook, remove the bobber. + # Else, it will be removed upon CMSG_LOOT_RELEASE. + if not fishing_node.hook_result: + self.caster.get_map().remove_object(fishing_node) def _handle_summoning_channel_end(self): # Specific handling of ritual of summoning interrupting. if not self.caster.channel_object: return channel_object = self.caster.get_map().get_surrounding_gameobject_by_guid(self.caster, self.caster.channel_object) - if not channel_object or channel_object.gobject_template.type != GameObjectTypes.TYPE_RITUAL: - return - channel_object.ritual_manager.channel_end(self.caster) + if isinstance(channel_object, RitualManager): + channel_object.channel_end(self.caster) def meets_casting_requisites(self, casting_spell) -> bool: # This method should only check resource costs (ie. power/combo/items). diff --git a/game/world/managers/objects/units/creature/CreatureManager.py b/game/world/managers/objects/units/creature/CreatureManager.py index 31b032149..c310017ea 100644 --- a/game/world/managers/objects/units/creature/CreatureManager.py +++ b/game/world/managers/objects/units/creature/CreatureManager.py @@ -292,16 +292,19 @@ def set_virtual_equipment(self, slot, item_id): VirtualItemsUtils.set_virtual_item(self, slot, item_id) def reset_virtual_equipment(self): - if self.creature_template.equipment_id > 0: - equip_template = WorldDatabaseManager.CreatureEquipmentHolder.creature_get_equipment_by_id( - self.creature_template.equipment_id - ) + equipment_id = self._get_equipment_id() + if equipment_id: + equip_template = WorldDatabaseManager.CreatureEquipmentHolder.creature_get_equipment_by_id(equipment_id) if equip_template: [VirtualItemsUtils.set_virtual_item(self, x, getattr(equip_template, f'equipentry{x + 1}')) for x in range(3)] return # Make sure its cleared if creature was morphed. [VirtualItemsUtils.set_virtual_item(self, x, 0) for x in range(3)] + def _get_equipment_id(self): + return self.addon.equipment_id if self.addon and self.addon.equipment_id \ + else self.creature_template.equipment_id + def set_faction(self, faction_id): self.faction = faction_id self.set_uint32(UnitFields.UNIT_FIELD_FACTIONTEMPLATE, self.faction) @@ -786,8 +789,17 @@ def set_npc_flag(self, flag, enable=True): def get_name(self): return self.creature_template.name + # override + def get_entry(self): + if self.entry: + return self.entry + if self.creature_template: + return self.creature_template.entry + return 0 + # override def respawn(self): + self.initialize_from_creature_template(self.creature_template) super().respawn() # override @@ -921,7 +933,6 @@ def get_debug_messages(self, requester=None): ] # override - # noinspection PyMethodMayBeStatic def get_creature_family(self): return self.creature_template.beast_family diff --git a/game/world/managers/objects/units/creature/items/VirtualItemUtils.py b/game/world/managers/objects/units/creature/items/VirtualItemUtils.py index dd38d2920..13819e36a 100644 --- a/game/world/managers/objects/units/creature/items/VirtualItemUtils.py +++ b/game/world/managers/objects/units/creature/items/VirtualItemUtils.py @@ -1,7 +1,10 @@ +import traceback + from database.world.WorldDatabaseManager import WorldDatabaseManager from game.world.managers.objects.units.creature.items.VirtualItemInfoHolder import VirtualItemInfoHolder from utils.ByteUtils import ByteUtils from utils.Formulas import UnitFormulas +from utils.Logger import Logger from utils.constants.ItemCodes import InventoryTypes from utils.constants.UpdateFields import UnitFields @@ -51,9 +54,15 @@ def set_virtual_item(creature_mgr, slot, item_entry): creature_mgr.virtual_item_info[slot] = VirtualItemInfoHolder() virtual_info = creature_mgr.virtual_item_info[slot] - creature_mgr.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_SLOT_DISPLAY + slot, virtual_info.display_id) - creature_mgr.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 0, virtual_info.info_packed) - creature_mgr.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 1, virtual_info.info_packed_2) + + # Some items packed data will overflow int, this usually happens when trying to equip non monster items + # to npcs via .setvirtualitem command. + try: + creature_mgr.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_SLOT_DISPLAY + slot, virtual_info.display_id) + creature_mgr.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 0, virtual_info.info_packed) + creature_mgr.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 1, virtual_info.info_packed_2) + except: + Logger.error(f'Item entry: [{item_entry}] {traceback.format_exc()}') if slot == 0: creature_mgr.set_weapon_reach(weapon_reach) diff --git a/game/world/managers/objects/units/movement/MovementInfo.py b/game/world/managers/objects/units/movement/MovementInfo.py index 56db067e8..2f9053617 100644 --- a/game/world/managers/objects/units/movement/MovementInfo.py +++ b/game/world/managers/objects/units/movement/MovementInfo.py @@ -89,7 +89,7 @@ def _remove_transport(self): def _get_transport(self): map_ = self.owner.get_map() - return map_.get_surrounding_gameobject_by_guid(self.owner, self.owner.transport_id).transport_manager + return map_.get_surrounding_gameobject_by_guid(self.owner, self.owner.transport_id) def _get_bytes(self): data = pack('<2Q9fI', self.owner.guid, self.owner.transport_id, self.owner.transport_location.x, diff --git a/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py b/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py index 86b6fe2bb..66bfd7714 100644 --- a/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py @@ -1,6 +1,6 @@ import time from utils.ConfigManager import config -from utils.constants.MiscCodes import MoveType, ScriptTypes, MoveFlags, ObjectTypeIds +from utils.constants.MiscCodes import MoveType, ScriptTypes, MoveFlags from database.world.WorldDatabaseManager import WorldDatabaseManager from game.world.managers.objects.units.movement.helpers.MovementWaypoint import MovementWaypoint @@ -13,7 +13,7 @@ def __init__(self, spline_callback, is_default=False, waypoints=None, speed=0, c is_single=False): super().__init__(move_type=MoveType.WAYPOINTS, spline_callback=spline_callback, is_default=is_default) self.creature_movement = None - self.is_single=is_single + self.is_single = is_single self.command_move_info = command_move_info self.speed = speed self.should_repeat = is_default diff --git a/game/world/managers/objects/units/player/PlayerManager.py b/game/world/managers/objects/units/player/PlayerManager.py index 3361b8416..a9030fb55 100644 --- a/game/world/managers/objects/units/player/PlayerManager.py +++ b/game/world/managers/objects/units/player/PlayerManager.py @@ -143,6 +143,7 @@ def __init__(self, # GM checks self.is_god = False + self.collision_cheat = False if self.session.account_mgr.is_gm(): self.set_gm_tag() @@ -402,6 +403,28 @@ def synchronize_db_player(self): self.player.money = self.coinage self.player.online = self.online + def toggle_collision(self): + self.collision_cheat = not self.collision_cheat + flags = MoveFlags.MOVEFLAG_DONTCOLLIDE if self.collision_cheat else MoveFlags.MOVEFLAG_NONE + + data = pack( + '