diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index ace88a432..d5334650a 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -10,7 +10,6 @@ from utils.constants.ItemCodes import InventorySlots from utils.constants.MiscCodes import HighGuid - DB_USER = os.getenv('MYSQL_USERNAME', config.Database.Connection.username) DB_PASSWORD = os.getenv('MYSQL_PASSWORD', config.Database.Connection.password) DB_HOST = os.getenv('MYSQL_HOST', config.Database.Connection.host) @@ -35,13 +34,23 @@ def realm_get_list(): # Account- @staticmethod - def account_try_login(username, password, ip): + def account_try_get(username): + realm_db_session = SessionHolder() + account_mgr = None + account = realm_db_session.query(Account).filter_by(name=username).first() + if account: + account_mgr = AccountManager(account) + realm_db_session.close() + return account_mgr + + @staticmethod + def account_try_login(username, password, ip, client_digest, server_digest): realm_db_session = SessionHolder() account = realm_db_session.query(Account).filter_by(name=username).first() status = -1 account_mgr = None if account: - if account.password == password: + if (password and account.password == password) or (client_digest and client_digest == server_digest): status = 1 account.ip = ip account_mgr = AccountManager(account) @@ -56,10 +65,14 @@ def account_try_login(username, password, ip): return status, account_mgr @staticmethod - def account_create(username, password, ip): + def account_create(username, password, ip, salt, verifier): realm_db_session = SessionHolder() account = Account(name=username, password=password, ip=ip, - gmlevel=int(config.Server.Settings.auto_create_gm_accounts)) + gmlevel=int(config.Server.Settings.auto_create_gm_accounts), + salt=salt, + verifier=verifier, + sessionkey="" + ) realm_db_session.add(account) realm_db_session.flush() realm_db_session.commit() @@ -67,6 +80,24 @@ def account_create(username, password, ip): realm_db_session.close() return AccountManager(account) + @staticmethod + def account_try_update_session_key(username, session_key): + realm_db_session = SessionHolder() + try: + account = realm_db_session.query(Account).filter_by(name=username).first() + if not account: + return False + + account.sessionkey = session_key + + realm_db_session.merge(account) + realm_db_session.flush() + realm_db_session.commit() + finally: + realm_db_session.close() + + return True + @staticmethod def account_try_update_password(username, old_password, new_password): realm_db_session = SessionHolder() diff --git a/database/realm/RealmModels.py b/database/realm/RealmModels.py index 5e87ad544..a91817fed 100644 --- a/database/realm/RealmModels.py +++ b/database/realm/RealmModels.py @@ -16,6 +16,9 @@ class Account(Base): password = Column(String(256), nullable=False) ip = Column(String(256), nullable=False) gmlevel = Column(TINYINT(4), nullable=False, server_default=text("1")) + salt = Column(String(256), nullable=False) + verifier = Column(String(256), nullable=False) + sessionkey = Column(String(256), nullable=False) class AppliedUpdate(Base): diff --git a/etc/config/config.yml.dist b/etc/config/config.yml.dist index 3c06693a6..476280764 100644 --- a/etc/config/config.yml.dist +++ b/etc/config/config.yml.dist @@ -1,5 +1,5 @@ Version: - current: 18 + current: 19 Database: Connection: @@ -15,6 +15,14 @@ Database: Server: Connection: # Change 0.0.0.0 for 127.0.0.1 if it doesn't work + Update: + host: 0.0.0.0 + port: 9081 + + Login: + host: 0.0.0.0 + port: 3724 + Realm: local_realm_id: 1 # id of the realm running on this machine (realmlist table) diff --git a/etc/databases/realm/updates/updates.sql b/etc/databases/realm/updates/updates.sql index 1b0dd08c9..acfad45b7 100644 --- a/etc/databases/realm/updates/updates.sql +++ b/etc/databases/realm/updates/updates.sql @@ -241,5 +241,15 @@ begin not atomic insert into applied_updates values ('120620241'); end if; + -- 10/01/2025 1 + if (select count(*) from applied_updates where id='100120251') = 0 then + ALTER TABLE `accounts` + ADD COLUMN `salt` VARCHAR(256) NOT NULL AFTER `gmlevel`, + ADD COLUMN `verifier` VARCHAR(256) NOT NULL AFTER `salt`, + ADD COLUMN `sessionkey` VARCHAR(256) NOT NULL AFTER `verifier`; + + insert into applied_updates values ('100120251'); + end if; + end $ delimiter ; diff --git a/etc/databases/world/updates/updates.sql b/etc/databases/world/updates/updates.sql index 2a1df2d85..ecaff325b 100644 --- a/etc/databases/world/updates/updates.sql +++ b/etc/databases/world/updates/updates.sql @@ -1171,5 +1171,24 @@ begin not atomic insert into applied_updates values ('041220241'); end if; + -- 02/05/2025 1 + if (select count(*) from applied_updates where id='020520251') = 0 then + -- Events list for Venture Co. Taskmaster + DELETE FROM `creature_ai_events` WHERE `creature_id`=2977; + INSERT INTO `creature_ai_events` (`id`, `creature_id`, `condition_id`, `event_type`, `event_inverse_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `action1_script`, `action2_script`, `action3_script`, `comment`) VALUES (297701, 2977, 0, 2, 0, 100, 0, 15, 0, 0, 0, 297701, 0, 0, 'Venture Co. Taskmaster - Flee at 15% HP'); + INSERT INTO `creature_ai_events` (`id`, `creature_id`, `condition_id`, `event_type`, `event_inverse_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `action1_script`, `action2_script`, `action3_script`, `comment`) VALUES (297702, 2977, 0, 11, 0, 100, 0, 0, 0, 0, 0, 297702, 0, 0, 'Venture Co. Taskmaster - Cast Torch Burn Upon Spawn'); + + DELETE FROM `creature_ai_scripts` WHERE `id`=297702; + INSERT INTO `creature_ai_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 + (297702, 0, 0, 15, 5680, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Venture Co. Taskmaster - Cast Torch Burn'); + + UPDATE `quest_template` SET `Details` = 'I would charge you with a task, $N.$B$BI was on my boat, rowing over the submerged ruins of Zoram, when naga attacked me, surging from the water and tearing at me with their claws! I fled, carrying what supplies I could to make this meager camp.$B$BBut when I reached the shore and ran... my prized possession was lost.$B$BPlease, $N, find the site of my ambush and search for an ancient statuette. It is the reason I have braved the dangers of the Zoram Strand.', `RewOrReqMoney` = '750' WHERE (`entry` = '1007'); + + UPDATE `creature_template` SET `level_min` = '15', `level_max` = '15' WHERE (`entry` = '3681'); + + UPDATE `spawns_creatures` SET `position_x` = '10686.3', `position_y` = '1917.5', `position_z` = '1336.62', `orientation` = '0.998' WHERE (`spawn_id` = '46193'); + + insert into applied_updates values ('020520251'); + end if; end $ delimiter ; \ No newline at end of file diff --git a/game/login/LoginManager.py b/game/login/LoginManager.py new file mode 100644 index 000000000..3afcaf7ac --- /dev/null +++ b/game/login/LoginManager.py @@ -0,0 +1,37 @@ +import socket +import threading +import traceback + +from game.login.LoginSessionStateHandler import LoginSessionStateHandler +from network.sockets.SocketBuilder import SocketBuilder +from utils.ConfigManager import config +from utils.Logger import Logger + + +class LoginManager: + @staticmethod + def start_login(running, login_server_ready): + login_host = config.Server.Connection.Login.host + login_port = config.Server.Connection.Login.port + with SocketBuilder.build_socket(login_host, login_port, timeout=2) as server_socket: + server_socket.listen() + real_binding = server_socket.getsockname() + Logger.success(f'Login server started, listening on {real_binding[0]}:{real_binding[1]}') + login_server_ready.value = 1 + + try: + while running.value: + try: + client_socket, client_address = server_socket.accept() + server_handler = LoginSessionStateHandler(client_socket, client_address) + auth_session_thread = threading.Thread(target=server_handler.handle) + auth_session_thread.daemon = True + auth_session_thread.start() + except socket.timeout: + pass # Non blocking. + except OSError: + Logger.warning(traceback.format_exc()) + except KeyboardInterrupt: + pass + + Logger.info("Login server turned off.") diff --git a/game/login/LoginSessionStateHandler.py b/game/login/LoginSessionStateHandler.py new file mode 100644 index 000000000..cf0f4e911 --- /dev/null +++ b/game/login/LoginSessionStateHandler.py @@ -0,0 +1,98 @@ +import socket +from network.packet.PacketReader import PacketReader +from utils.Logger import Logger + +MAX_PACKET_BYTES = 4096 + + +class LoginSessionStateHandler: + def __init__(self, client_socket, client_address): + self.client_socket = client_socket + self.client_address = client_address + + def handle(self): + try: + self.keep_alive = True + self.client_socket.settimeout(120) # 2 minutes timeout should be more than enough. + + while self.receive(self.client_socket) != -1 and self.keep_alive: + continue + + finally: + self.disconnect() + + def receive(self, sck): + try: + reader = self.receive_client_message(sck) + if reader and self.keep_alive and reader.opcode: + return self.process_incoming(reader) + else: + return -1 + except (socket.timeout, OSError, ConnectionResetError, ValueError): + self.disconnect() + return -1 + + def process_incoming(self, reader): + from game.world.opcode_handling.Definitions import Definitions + res = -1 + try: + handler, found = Definitions.get_handler_from_packet(self, reader.opcode) + if handler: + res = handler(self, reader) + if res == 0: + Logger.debug(f'[{self.client_address[0]}] Handling {reader.opcode_str()}') + elif res == 1: + Logger.debug(f'[{self.client_address[0]}] Ignoring {reader.opcode_str()}') + elif res < 0: + Logger.warning(f'[{self.client_address[0]}] Handling {reader.opcode_str()} failed.') + res = -1 + elif not found: + Logger.warning(f'[{self.client_address[0]}] Received unknown data: {reader.data}') + except: + pass + + return res + + def receive_client_message(self, sck): + header_bytes = self.receive_all(sck, 6) # 6 = header size + if not header_bytes: + return None + + reader = PacketReader(header_bytes) + reader.data = self.receive_all(sck, int(reader.size)) + return reader + + def receive_all(self, sck, expected_size): + # Prevent wrong size because of malformed packets. + if expected_size <= 0: + return b'' + + # Try to fill at once. + received = sck.recv(expected_size) + if not received: + return b'' + + # We got what we expect, return buffer. + if received == expected_size: + return received + + # If we got incomplete data, request missing payload. + buffer = bytearray(received) + current_buffer_size = len(buffer) + while current_buffer_size < expected_size: + received = sck.recv(expected_size - current_buffer_size) + if not received: + return b'' + buffer.extend(received) # Keep appending to our buffer until we're done. + current_buffer_size = len(buffer) + # Avoid handling any packet that's above the maximum packet size. + if current_buffer_size > MAX_PACKET_BYTES: + return b'' + return buffer + + def disconnect(self): + try: + self.client_socket.shutdown(socket.SHUT_RDWR) + self.client_socket.close() + except OSError: + pass \ No newline at end of file diff --git a/tools/map_extractor/__init__.py b/game/login/__init__.py similarity index 100% rename from tools/map_extractor/__init__.py rename to game/login/__init__.py diff --git a/game/realm/AccountManager.py b/game/realm/AccountManager.py index de60ea3af..6d3847273 100644 --- a/game/realm/AccountManager.py +++ b/game/realm/AccountManager.py @@ -1,10 +1,21 @@ +import os +from struct import pack + +from network.packet.PacketWriter import PacketWriter +from utils.Srp6 import Srp6 from utils.constants import CustomCodes +from utils.constants.AuthCodes import Srp6ResponseType, AuthCode class AccountManager(object): def __init__(self, account): self.account = account + self._server_public_key = b'' + self._server_private_key = b'' + self._client_public_key = b'' + self._session_key = b'' + self._client_server_proof = b'' def get_security_level(self) -> CustomCodes.AccountSecurityLevel: return self.account.gmlevel @@ -17,3 +28,44 @@ def is_gm(self): def is_dev(self): return self.get_security_level() >= CustomCodes.AccountSecurityLevel.DEV + + # Srp6 - AuthSession. + + def update_server_public_private_keys(self): + self._server_private_key = os.urandom(32) + self._server_public_key = Srp6.calculate_server_public_key(self.get_verifier_bytes(), self._server_private_key) + + def get_salt_bytes(self) -> bytes: + return bytes.fromhex(self.account.salt) + + def get_verifier_bytes(self) -> bytes: + return bytes.fromhex(self.account.verifier) + + def calculate_client_server_proof(self, client_public_key): + self._client_public_key = client_public_key + u = Srp6.calculate_u(client_public_key, self._server_public_key) + s_key = Srp6.calculate_server_s_key(client_public_key, self.get_verifier_bytes(), u, self._server_private_key) + self._session_key = Srp6.calculate_interleaved(s_key) + self._client_server_proof = Srp6.calculate_client_proof(Srp6.xorNg, self.account.name, self._session_key, + client_public_key, self._server_public_key, + self.get_salt_bytes()) + return self._client_server_proof + + def get_srp6_server_proof_packet(self) -> bytes: + s_m2 = Srp6.calculate_server_proof(self._client_public_key, self._client_server_proof, self._session_key) + data = pack('<2B', AuthCode.AUTH_OK, Srp6ResponseType.AuthProof) + data += s_m2 + data += pack(' bytes: + data = pack('<2B', AuthCode.AUTH_OK, Srp6ResponseType.AuthChallenge) + data += pack(' tag {"enabled" if enable else "disabled"}.' + @staticmethod + def create_account(args): + args = str(args).strip().split() + if len(args) != 2: + return -1, 'please use it like: createacc username password' + username, password = args + salt = os.urandom(32) + verifier = Srp6.calculate_password_verifier(username, password, salt) + account = RealmDatabaseManager.account_create(username, hashlib.sha256(password.encode('utf-8')).hexdigest(), + "127.0.0.1", salt.hex(), verifier.hex()) + if not account: + return -1, 'unable to create account' + return 0, 'account created' + + +CONSOLE_COMMAND_DEFINITIONS = { + 'createacc' : [CommandManager.create_account, 'create srp6 account'] +} + PLAYER_COMMAND_DEFINITIONS = { 'help': [CommandManager.help, 'print this message'], diff --git a/game/world/managers/maps/MapManager.py b/game/world/managers/maps/MapManager.py index f2983df35..64e5b4c65 100644 --- a/game/world/managers/maps/MapManager.py +++ b/game/world/managers/maps/MapManager.py @@ -18,6 +18,7 @@ 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.GitUtils import GitUtils from utils.Logger import Logger from utils.PathManager import PathManager from utils.constants.MiscCodes import MapsNoNavs, MapTileStates @@ -239,6 +240,16 @@ def validate_map_files(): return True + @staticmethod + def validate_namigator_bindings(): + if not config.Server.Settings.use_nav_tiles: + return True + + if not GitUtils.check_download_namigator_bindings(): + return False + + return True + @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) diff --git a/game/world/managers/maps/helpers/Mapbuild.py b/game/world/managers/maps/helpers/Mapbuild.py new file mode 100644 index 000000000..e2cff34da --- /dev/null +++ b/game/world/managers/maps/helpers/Mapbuild.py @@ -0,0 +1,24 @@ +# Interface implemented in C++, to avoid IDE errors basically (not actually mandatory). +class Mapbuild: + + # Builds all gameobjects. Must be called before build_map. + def build_bvh(self, data_path, output_path, workers): + pass + + # Builds a specific map. `build_bvh` must be called before this function. + def build_map(self, data_path, output_path, map_name, threads, go_csv): + pass + + # Build a specific ADT. + def build_adt(self, data_path, output_path, map_name, x, y, go_csv): + pass + + # Checks if map files exist. If `True` the map will not need to be built. + def map_files_exist(self, output_path, map_name): + pass + + # Checks if gameobjects exist. If `True` the gameobjects will not need to be built. + def bvh_files_exist(self, output_path): + pass + + diff --git a/game/world/managers/objects/ObjectManager.py b/game/world/managers/objects/ObjectManager.py index 75ef65618..b6e1da242 100644 --- a/game/world/managers/objects/ObjectManager.py +++ b/game/world/managers/objects/ObjectManager.py @@ -314,57 +314,32 @@ def is_aura_field(self, index): return UnitFields.UNIT_FIELD_AURA <= index <= UnitFields.UNIT_FIELD_AURA + 55 def set_int32(self, index, value, force=False): - if force or self.update_packet_factory.should_update(index, value, 'i'): - self.update_packet_factory.update(index, value, 'i') - if force and self.is_in_world(): # Changes should apply immediately. - self.get_map().update_object(self, has_changes=True) - return True, force - return False, force + return self._set_value(index, value, 'i', force) + + def set_uint32(self, index, value, force=False): + return self._set_value(index, value, 'I', force) + + def set_int64(self, index, value, force=False): + return self._set_value(index, value, 'q', force) + + def set_uint64(self, index, value, force=False): + return self._set_value(index, value, 'Q', force) + + def set_float(self, index, value, force=False): + return self._set_value(index, value, 'f', force) def get_int32(self, index): return self._get_value_by_type_at('i', index) - def set_uint32(self, index, value, force=False): - if force or self.update_packet_factory.should_update(index, value, 'I'): - self.update_packet_factory.update(index, value, 'I') - if force and self.is_in_world(): # Changes should apply immediately. - self.get_map().update_object(self, has_changes=True) - return True, force - return False, force - def get_uint32(self, index): return self._get_value_by_type_at('I', index) - def set_int64(self, index, value, force=False): - if force or self.update_packet_factory.should_update(index, value, 'q'): - self.update_packet_factory.update(index, value, 'q') - if force and self.is_in_world(): # Changes should apply immediately. - self.get_map().update_object(self, has_changes=True) - return True, force - return False, force - def get_int64(self, index): return self._get_value_by_type_at('q', index) - def set_uint64(self, index, value, force=False): - if force or self.update_packet_factory.should_update(index, value, 'Q'): - self.update_packet_factory.update(index, value, 'Q') - if force and self.is_in_world(): # Changes should apply immediately. - self.get_map().update_object(self, has_changes=True) - return True, force - return False, force - def get_uint64(self, index): return self._get_value_by_type_at('Q', index) - def set_float(self, index, value, force=False): - if force or self.update_packet_factory.should_update(index, value, 'f'): - self.update_packet_factory.update(index, value, 'f') - if force and self.is_in_world(): # Changes should apply immediately. - self.get_map().update_object(self, has_changes=True) - return True, force - return False, force - def get_float(self, index): return self._get_value_by_type_at('f', index) @@ -383,6 +358,15 @@ def _get_value_by_type_at(self, value_type, index): return unpack(f'<{value_type}', value)[0] + def _set_value(self, index, value, value_type, force=False): + force = force and self.is_player() + if force or self.update_packet_factory.should_update(index, value, value_type): + self.update_packet_factory.update(index, value, value_type) + if force and self.is_in_world(): # Changes should apply immediately. + self.get_map().update_object(self, has_changes=True) + return True, force + return False, force + # override def update(self, now): pass diff --git a/game/world/managers/objects/spell/CastingSpell.py b/game/world/managers/objects/spell/CastingSpell.py index cfcdd6c29..d2a2fbf6a 100644 --- a/game/world/managers/objects/spell/CastingSpell.py +++ b/game/world/managers/objects/spell/CastingSpell.py @@ -11,13 +11,13 @@ from game.world.managers.objects.item.ItemManager import ItemManager from game.world.managers.objects.spell import ExtendedSpellData from game.world.managers.objects.spell.EffectTargets import TargetMissInfo, EffectTargets -from game.world.managers.objects.spell.ExtendedSpellData import TotemHelpers +from game.world.managers.objects.spell.ExtendedSpellData import TotemHelpers, SpellThreatMechanics from game.world.managers.objects.units.DamageInfoHolder import DamageInfoHolder from game.world.managers.objects.units.player.StatManager import UnitStats from game.world.managers.objects.spell.SpellEffect import SpellEffect from network.packet.PacketWriter import PacketWriter from utils.constants.ItemCodes import ItemClasses, ItemSubClasses -from utils.constants.MiscCodes import ObjectTypeFlags, AttackTypes, HitInfo +from utils.constants.MiscCodes import AttackTypes, HitInfo from utils.constants.OpCodes import OpCode from utils.constants.SpellCodes import SpellState, SpellCastFlags, SpellTargetMask, SpellAttributes, SpellAttributesEx, \ AuraTypes, SpellEffects, SpellInterruptFlags, SpellImplicitTargets, SpellImmunity, SpellSchoolMask, SpellHitFlags, \ @@ -291,7 +291,11 @@ def is_far_sight(self): return self.spell_entry.AttributesEx & SpellAttributesEx.SPELL_ATTR_EX_FARSIGHT def generates_threat(self): - return not self.spell_entry.AttributesEx & SpellAttributesEx.SPELL_ATTR_EX_NO_THREAT + return (not self.spell_entry.AttributesEx & SpellAttributesEx.SPELL_ATTR_EX_NO_THREAT + and SpellThreatMechanics.spell_should_generate_threat(self.spell_entry.ID)) + + def generates_threat_on_miss(self): + return self.spell_entry.AttributesEx & SpellAttributesEx.SPELL_ATTR_EX_THREAT_ON_MISS def requires_implicit_initial_unit_target(self): # Some spells are self casts, but require an implicit unit target when casted. diff --git a/game/world/managers/objects/spell/ExtendedSpellData.py b/game/world/managers/objects/spell/ExtendedSpellData.py index 1ae63b7b1..48dbff140 100644 --- a/game/world/managers/objects/spell/ExtendedSpellData.py +++ b/game/world/managers/objects/spell/ExtendedSpellData.py @@ -312,6 +312,8 @@ class SpellEffectMechanics: # Sleep uses stun but is a distinct mechanic; use IDs instead. _SLEEP_MECHANIC_SPELLS = (700, 1090, 2937) + _SAP_SPELLS = (2070, 6770, 6771) + @staticmethod def get_mechanic_for_aura_effect(aura_type, spell_id) -> Optional[SpellMechanic]: if not aura_type: @@ -322,3 +324,15 @@ def get_mechanic_for_aura_effect(aura_type, spell_id) -> Optional[SpellMechanic] return SpellMechanic.MECHANIC_SLEEP if \ spell_id in SpellEffectMechanics._SLEEP_MECHANIC_SPELLS else None + +class SpellThreatMechanics: + _NON_COMBAT_STUN_SPELLS = ( + 700, 1090, 2937, # Sleep. + 2070, 6770, 6771, # Sap. + 6358 # Seduction. + ) + + # According to evidence from screenshots, neither Sleep, Sap nor Seduction gets the player in combat. + @staticmethod + def spell_should_generate_threat(spell_id) -> bool: + return spell_id not in SpellThreatMechanics._NON_COMBAT_STUN_SPELLS \ No newline at end of file diff --git a/game/world/managers/objects/spell/SpellManager.py b/game/world/managers/objects/spell/SpellManager.py index 0453d41b6..3c8f99f84 100644 --- a/game/world/managers/objects/spell/SpellManager.py +++ b/game/world/managers/objects/spell/SpellManager.py @@ -476,7 +476,7 @@ def apply_spell_effects(self, casting_spell: CastingSpell, remove=False, update= if info.result == SpellMissReason.MISS_REASON_NONE: SpellEffectHandler.apply_effect(casting_spell, effect, spell_caster, spell_target) - elif target.is_unit() and casting_spell.generates_threat() and \ + elif target.is_unit() and casting_spell.generates_threat_on_miss() and \ casting_spell.spell_caster.can_attack_target(target): # Add threat for failed hostile casts. target.threat_manager.add_threat(casting_spell.spell_caster) diff --git a/game/world/managers/objects/units/UnitManager.py b/game/world/managers/objects/units/UnitManager.py index 6386ef4e5..4c14c1f11 100644 --- a/game/world/managers/objects/units/UnitManager.py +++ b/game/world/managers/objects/units/UnitManager.py @@ -1215,11 +1215,12 @@ def set_rooted(self, active=True, index=-1) -> bool: def set_stunned(self, active=True, index=-1) -> bool: self.set_rooted(active, index) - was_stunned = self.unit_state & UnitStates.STUNNED - is_stunned = self.set_unit_state(UnitStates.STUNNED, active, index) - self.set_unit_flag(UnitFlags.UNIT_FLAG_DISABLE_ROTATE, active, index) + was_stunned = bool(self.unit_state & UnitStates.STUNNED) + is_stunned = bool(self.set_unit_state(UnitStates.STUNNED, active, index)) if not was_stunned and is_stunned: + # Force move behavior stop. + self.movement_manager.stop() self.spell_manager.remove_casts(remove_active=False) self.set_current_target(0) elif was_stunned and not is_stunned: @@ -1227,6 +1228,8 @@ def set_stunned(self, active=True, index=-1) -> bool: if self.combat_target and self.combat_target.is_alive: self.set_current_target(self.combat_target.guid) + self.set_unit_flag(UnitFlags.UNIT_FLAG_DISABLE_ROTATE, active, index) + return is_stunned def remove_all_unit_flags(self, clear_effects=True): @@ -1910,6 +1913,9 @@ def notify_move_in_line_of_sight(self): surrounding_units = surrounding_units.values() for unit in surrounding_units: + if unit.unit_state & UnitStates.STUNNED or unit.unit_flags & UnitFlags.UNIT_FLAG_PACIFIED: + continue + unit_is_player = unit.is_player() unit_has_ooc_los_events = not unit_is_player and unit.object_ai.ai_event_handler.has_ooc_los_events() diff --git a/game/world/managers/objects/units/movement/behaviors/ChaseMovement.py b/game/world/managers/objects/units/movement/behaviors/ChaseMovement.py index 4e9926726..ad88fbc0b 100644 --- a/game/world/managers/objects/units/movement/behaviors/ChaseMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/ChaseMovement.py @@ -95,7 +95,7 @@ def can_remove(self): # override def reset(self): - # Make sure the last known position gets updated. if self.spline: + # Make sure the last known position gets updated. self.spline.update_to_now() - self.spline = None + self.spline = None diff --git a/game/world/managers/objects/units/movement/behaviors/GroupMovement.py b/game/world/managers/objects/units/movement/behaviors/GroupMovement.py index a619cc5b4..bff06beeb 100644 --- a/game/world/managers/objects/units/movement/behaviors/GroupMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/GroupMovement.py @@ -56,7 +56,10 @@ def on_new_position(self, new_position, waypoint_completed, remaining_waypoints) # override def reset(self): - self.spline = None + if self.spline: + # Make sure the last known position gets updated. + self.spline.update_to_now() + self.spline = None self.wait_time_seconds = 0 self.last_waypoint_movement = 0 diff --git a/game/world/managers/objects/units/movement/behaviors/PetMovement.py b/game/world/managers/objects/units/movement/behaviors/PetMovement.py index b546a8281..710f3d698 100644 --- a/game/world/managers/objects/units/movement/behaviors/PetMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/PetMovement.py @@ -76,7 +76,10 @@ def can_remove(self): # override def reset(self): - self.spline = None + if self.spline: + # Make sure the last known position gets updated. + self.spline.update_to_now() + self.spline = None self.pet_range_move = None # External call. diff --git a/game/world/managers/objects/units/movement/behaviors/WanderingMovement.py b/game/world/managers/objects/units/movement/behaviors/WanderingMovement.py index e893c4bba..73b73d809 100644 --- a/game/world/managers/objects/units/movement/behaviors/WanderingMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/WanderingMovement.py @@ -38,7 +38,10 @@ def update(self, now, elapsed): # override def reset(self): - self.spline = None + if self.spline: + # Make sure the last known position gets updated. + self.spline.update_to_now() + self.spline = None self.wait_time_seconds = randint(1, 12) self.last_wandering_movement = time.time() diff --git a/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py b/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py index 66bfd7714..0f203f514 100644 --- a/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/WaypointMovement.py @@ -103,7 +103,10 @@ def on_new_position(self, new_position, waypoint_completed, remaining_waypoints) # override def reset(self): - self.spline = None + if self.spline: + # Make sure the last known position gets updated. + self.spline.update_to_now() + self.spline = None self.wait_time_seconds = 0 self.last_waypoint_movement = 0 diff --git a/game/world/managers/objects/units/player/PlayerManager.py b/game/world/managers/objects/units/player/PlayerManager.py index 6eef8bc84..59f010919 100644 --- a/game/world/managers/objects/units/player/PlayerManager.py +++ b/game/world/managers/objects/units/player/PlayerManager.py @@ -762,7 +762,7 @@ def set_stealthed(self, active=True, index=-1): stealthed = super().set_stealthed(active, index) if not stealthed: # Notify surrounding units about fading stealth for proximity aggro. - self._on_relocation() + self.pending_relocation = True # override def set_sanctuary(self, active=True, time_secs=0): diff --git a/game/world/opcode_handling/Definitions.py b/game/world/opcode_handling/Definitions.py index 2138a413a..b32e717e4 100644 --- a/game/world/opcode_handling/Definitions.py +++ b/game/world/opcode_handling/Definitions.py @@ -171,6 +171,9 @@ from utils.constants.OpCodes import OpCode HANDLER_DEFINITIONS = { + # Auth + OpCode.CMSG_AUTH_SRP6_BEGIN: AuthSessionHandler.handle_srp6_begin, + OpCode.CMSG_AUTH_SRP6_PROOF: AuthSessionHandler.handle_srp6_proof, OpCode.CMSG_AUTH_SESSION: AuthSessionHandler.handle, OpCode.CMSG_PING: PingHandler.handle, OpCode.CMSG_CHAR_ENUM: CharEnumHandler.handle, diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index f44a97a07..f96135d22 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -1,48 +1,130 @@ from database.realm.RealmDatabaseManager import * +from game.world import WorldManager from game.world.WorldSessionStateHandler import WorldSessionStateHandler from network.packet.PacketReader import * from network.packet.PacketWriter import * +from utils.Srp6 import Srp6 from utils.constants.AuthCodes import * class AuthSessionHandler(object): @staticmethod - def handle(world_session, reader): - version, login = unpack( - 'H', len(data)) + data + @staticmethod def get_packet(opcode, data=b''): if data is None: diff --git a/network/sockets/SocketBuilder.py b/network/sockets/SocketBuilder.py new file mode 100644 index 000000000..b8b506c73 --- /dev/null +++ b/network/sockets/SocketBuilder.py @@ -0,0 +1,15 @@ +import socket + +class SocketBuilder: + + @staticmethod + def build_socket(address, port, timeout): + socket_ = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + # Use SO_REUSEADDR if SO_REUSEPORT doesn't exist. + except AttributeError: + socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + socket_.bind((address, port)) + socket_.settimeout(timeout) + return socket_ diff --git a/tools/map_extractor/definitions/chunks/__init__.py b/network/sockets/__init__.py similarity index 100% rename from tools/map_extractor/definitions/chunks/__init__.py rename to network/sockets/__init__.py diff --git a/requirements.txt b/requirements.txt index b26b524a8..b125dd7ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ colorama SQLAlchemy pymysql apscheduler +requests diff --git a/tools/extractors/Extractor.py b/tools/extractors/Extractor.py new file mode 100644 index 000000000..b96a442d8 --- /dev/null +++ b/tools/extractors/Extractor.py @@ -0,0 +1,34 @@ +import os + +from tools.extractors.MapExtractor import MapExtractor +from tools.extractors.NavExtractor import NavExtractor +from utils.ConfigManager import config +from utils.Logger import Logger + +WOW_DATA_FOLDER = 'Data/' +WOW_MAPS_FOLDER = 'World/Maps/' + +class Extractor: + + @staticmethod + def run(): + # Validate WoW root. + if not config.Extractor.Maps.wow_root_path: + Logger.error('No wow root path provided, check config.yml. (World of Warcraft base directory)') + return + # Validate its existence. + elif not os.path.exists(config.Extractor.Maps.wow_root_path): + Logger.error(f'Data path "{config.Extractor.Maps.wow_root_path}" does not exist.') + return + + # Validate /Data/. + data_path = os.path.join(config.Extractor.Maps.wow_root_path, WOW_DATA_FOLDER) + if not os.path.exists(data_path): + Logger.error(f'Unable to locate {data_path}.') + return + + if input('Extract .map files? [Y/N]').lower() == 'y': + MapExtractor.run(data_path, WOW_MAPS_FOLDER) + + if input('Extract .nav files? [Y/N]').lower() == 'y': + NavExtractor.run(data_path) diff --git a/tools/map_extractor/MapExtractor.py b/tools/extractors/MapExtractor.py similarity index 68% rename from tools/map_extractor/MapExtractor.py rename to tools/extractors/MapExtractor.py index dbc1137ca..53fda6adc 100644 --- a/tools/map_extractor/MapExtractor.py +++ b/tools/extractors/MapExtractor.py @@ -1,55 +1,40 @@ import os + + from utils.Logger import Logger -from utils.ConfigManager import config from utils.PathManager import PathManager -from tools.map_extractor.definitions.Wdt import Wdt -from tools.map_extractor.pydbclib.structs.Map import Map -from tools.map_extractor.pydbclib.DbcReader import DbcReader -from tools.map_extractor.helpers.DataHolders import DataHolders -from tools.map_extractor.pympqlib.MpqArchive import MpqArchive -from tools.map_extractor.pydbclib.structs.AreaTable import AreaTable +from tools.extractors.definitions.Wdt import Wdt +from tools.extractors.pydbclib.structs.Map import Map +from tools.extractors.pydbclib.DbcReader import DbcReader +from tools.extractors.helpers.DataHolders import DataHolders +from tools.extractors.pympqlib.MpqArchive import MpqArchive +from tools.extractors.pydbclib.structs.AreaTable import AreaTable + -WOW_DATA_FOLDER = 'Data' -WOW_MAPS_FOLDER = 'World/Maps' REQUIRED_DBC = 'dbc.MPQ' class MapExtractor: @staticmethod - def run(): - # Validate WoW root. - if not config.Extractor.Maps.wow_root_path: - Logger.error('No wow root path provided. (World of Warcraft base directory)') - exit() - # Validate its existence. - elif not os.path.exists(config.Extractor.Maps.wow_root_path): - Logger.error(f'Data path "{config.Extractor.Maps.wow_root_path}" does not exist.') - exit() - - # Validate /Data/. - data_path = os.path.join(config.Extractor.Maps.wow_root_path, WOW_DATA_FOLDER) - if not os.path.exists(data_path): - Logger.error(f'Unable to locate {data_path}.') - exit() + def run(data_path, wow_maps_folder): + # Validate /etc/maps. + map_files_path = PathManager.get_maps_path() + if not os.path.exists(map_files_path): + Logger.error(f'Unable to locate {map_files_path}.') + return # Validate dbc.MPQ. dbc_path = os.path.join(data_path, REQUIRED_DBC) if not os.path.exists(dbc_path): Logger.error(f'Unable to locate {dbc_path}.') - exit() + return - maps_path = os.path.join(data_path, WOW_MAPS_FOLDER) + maps_path = os.path.join(data_path, wow_maps_folder) if not os.path.exists(dbc_path): - Logger.error(f'Unable to locate {WOW_MAPS_FOLDER}.') - exit() - - # Validate /etc/maps. - map_files_path = PathManager.get_maps_path() - if not os.path.exists(map_files_path): - Logger.error(f'Unable to locate {map_files_path}.') - exit() + Logger.error(f'Unable to locate {wow_maps_folder}.') + return # Flush existent files. filelist = [f for f in os.listdir(map_files_path) if f.endswith(".map")] @@ -58,7 +43,7 @@ def run(): if input().lower() in ['y', '']: [os.remove(os.path.join(map_files_path, file)) for file in filelist] else: - exit() + return # Extract available maps and area tables from dbc. with MpqArchive(dbc_path) as archive: @@ -82,11 +67,11 @@ def run(): # Validate we have maps. if not DataHolders.MAPS: Logger.error(f'Unable to read maps from {dbc_path}.') - exit() + return if not DataHolders.AREA_TABLES_BY_MAP: Logger.error(f'Unable to read area tables from {dbc_path}.') - exit() + return for dbc_map in DataHolders.get_maps(): # Interested in ADT based maps, not WMO based. diff --git a/tools/extractors/NavExtractor.py b/tools/extractors/NavExtractor.py new file mode 100644 index 000000000..314ab7510 --- /dev/null +++ b/tools/extractors/NavExtractor.py @@ -0,0 +1,108 @@ +import os.path +import traceback +import multiprocessing +from time import sleep + +from utils.GitUtils import GitUtils +from utils.Logger import Logger +from utils.PathManager import PathManager + +class NavExtractor: + + maps_navs = { + 'DeadminesInstance' : 36, + 'GnomeragonInstance' : 1, + 'Monastery' : 1, + 'RazorfenDowns' : 1, + 'RazorfenKraulInstance' : 6, + 'Blackfathom' : 1, + 'StormwindJail' : 1, + 'StormwindPrison' : 1, + 'SunkenTemple' : 1, + 'Uldaman' : 1, + 'WailingCaverns' : 1, + 'Kalidar' : 56, + 'Shadowfang' : 25, + 'Kalimdor' : 951, + 'Azeroth' : 685 + } + + @staticmethod + def run(data_path): + if not GitUtils.check_download_namigator_bindings(): + Logger.error(f'Unable to locate/download namigator bindings.') + return + + try: + from namigator import mapbuild + except: + Logger.warning(traceback.format_exc()) + return + + # Internal namigator 'Nav' folder. + nav_path = os.path.join(PathManager.get_navs_path(), 'Nav') + if not os.path.exists(nav_path): + os.mkdir(nav_path) + + try: + threads = int(input("Number of threads?:")) + Logger.info('[NavExtractor] Building bhv files...') + NavExtractor._extract_bhv(data_path, mapbuild) + + Logger.info(f'[NavExtractor] Building navs, using {threads} threads.') + for map_name in NavExtractor.maps_navs.keys(): + map_path = os.path.join(nav_path, map_name) + if not os.path.exists(map_path): + os.mkdir(map_path) + + # Extractor process. + process = multiprocessing.Process(target=NavExtractor._extract_map, + args=(data_path, map_name, threads, mapbuild)) + process.start() + + # Wait for process. + NavExtractor._show_progress(process, map_name) + except: + Logger.warning(traceback.format_exc()) + + @staticmethod + def _show_progress(process, map_name): + Logger.info(f'[NavExtractor] Building nav files for {map_name} ...') + total = NavExtractor.maps_navs[map_name] + progress = 0 + while process.is_alive(): + progress = NavExtractor._get_progress(map_name) + if progress: + Logger.progress(f'[NavExtractor] Building nav files for {map_name} ...', progress, total) + sleep(1) + + # Final progress. + if progress and progress != total: + progress = NavExtractor._get_progress(map_name) + Logger.progress(f'[NavExtractor] Building nav files for {map_name} ...', progress, total) + + @staticmethod + def _get_progress(map_name): + return len(os.listdir(os.path.join(PathManager.get_navs_path(), f'Nav/{map_name}/'))) + + @staticmethod + def _extract_bhv(data_path, mapbuild): + try: + if mapbuild.bvh_files_exist(PathManager.get_navs_path()): + Logger.info(f'[NavExtractor] Skipping bhv files, already found.') + return 0 + count = mapbuild.build_bvh(data_path, PathManager.get_navs_path(), 1) + Logger.info(f'[NavExtractor] Successfully extracted {count} bhv files.') + return count + except: + Logger.warning(traceback.format_exc()) + + @staticmethod + def _extract_map(data_path, map_name, threads, mapbuild): + try: + if mapbuild.map_files_exist(PathManager.get_navs_path(), map_name): + Logger.info(f'[NavExtractor] Skipping map {map_name}, already found.') + return + mapbuild.build_map(data_path[:-1], PathManager.get_navs_path(), map_name, threads, '') + except: + Logger.warning(traceback.format_exc()) diff --git a/tools/map_extractor/definitions/enums/__init__.py b/tools/extractors/__init__.py similarity index 100% rename from tools/map_extractor/definitions/enums/__init__.py rename to tools/extractors/__init__.py diff --git a/tools/map_extractor/definitions/Adt.py b/tools/extractors/definitions/Adt.py similarity index 90% rename from tools/map_extractor/definitions/Adt.py rename to tools/extractors/definitions/Adt.py index aaf92c746..d9378d873 100644 --- a/tools/map_extractor/definitions/Adt.py +++ b/tools/extractors/definitions/Adt.py @@ -3,12 +3,12 @@ from utils.Logger import Logger from utils.PathManager import PathManager from network.packet.PacketWriter import PacketWriter -from tools.map_extractor.helpers.Constants import Constants -from tools.map_extractor.helpers.DataHolders import DataHolders -from tools.map_extractor.helpers.HeightField import HeightField -from tools.map_extractor.helpers.LiquidAdtWriter import LiquidAdtWriter -from tools.map_extractor.definitions.chunks.TileHeader import TileHeader -from tools.map_extractor.definitions.chunks.TileInformation import TileInformation +from tools.extractors.helpers.Constants import Constants +from tools.extractors.helpers.DataHolders import DataHolders +from tools.extractors.helpers.HeightField import HeightField +from tools.extractors.helpers.LiquidAdtWriter import LiquidAdtWriter +from tools.extractors.definitions.chunks.TileHeader import TileHeader +from tools.extractors.definitions.chunks.TileInformation import TileInformation class Adt: diff --git a/tools/map_extractor/definitions/Wdt.py b/tools/extractors/definitions/Wdt.py similarity index 92% rename from tools/map_extractor/definitions/Wdt.py rename to tools/extractors/definitions/Wdt.py index 6a6e396f3..f9f62bec1 100644 --- a/tools/map_extractor/definitions/Wdt.py +++ b/tools/extractors/definitions/Wdt.py @@ -2,10 +2,10 @@ from game.world.managers.maps.helpers.Constants import BLOCK_SIZE from utils.Logger import Logger -from tools.map_extractor.definitions.Adt import Adt -from tools.map_extractor.helpers.Constants import Constants -from tools.map_extractor.definitions.chunks.TileHeader import TileHeader -from tools.map_extractor.definitions.reader.StreamReader import StreamReader +from tools.extractors.definitions.Adt import Adt +from tools.extractors.helpers.Constants import Constants +from tools.extractors.definitions.chunks.TileHeader import TileHeader +from tools.extractors.definitions.reader.StreamReader import StreamReader class Wdt: diff --git a/tools/map_extractor/definitions/reader/__init__.py b/tools/extractors/definitions/__init__.py similarity index 100% rename from tools/map_extractor/definitions/reader/__init__.py rename to tools/extractors/definitions/__init__.py diff --git a/tools/map_extractor/definitions/chunks/MCLQ.py b/tools/extractors/definitions/chunks/MCLQ.py similarity index 90% rename from tools/map_extractor/definitions/chunks/MCLQ.py rename to tools/extractors/definitions/chunks/MCLQ.py index 8a8248916..310c3cd18 100644 --- a/tools/map_extractor/definitions/chunks/MCLQ.py +++ b/tools/extractors/definitions/chunks/MCLQ.py @@ -1,4 +1,4 @@ -from tools.map_extractor.definitions.enums.LiquidFlags import LiquidFlags +from tools.extractors.definitions.enums.LiquidFlags import LiquidFlags class MCLQ: diff --git a/tools/map_extractor/definitions/chunks/MCVT.py b/tools/extractors/definitions/chunks/MCVT.py similarity index 100% rename from tools/map_extractor/definitions/chunks/MCVT.py rename to tools/extractors/definitions/chunks/MCVT.py diff --git a/tools/map_extractor/definitions/chunks/TileHeader.py b/tools/extractors/definitions/chunks/TileHeader.py similarity index 100% rename from tools/map_extractor/definitions/chunks/TileHeader.py rename to tools/extractors/definitions/chunks/TileHeader.py diff --git a/tools/map_extractor/definitions/chunks/TileInformation.py b/tools/extractors/definitions/chunks/TileInformation.py similarity index 87% rename from tools/map_extractor/definitions/chunks/TileInformation.py rename to tools/extractors/definitions/chunks/TileInformation.py index b23f81382..c30bcdf1c 100644 --- a/tools/map_extractor/definitions/chunks/TileInformation.py +++ b/tools/extractors/definitions/chunks/TileInformation.py @@ -1,6 +1,6 @@ -from tools.map_extractor.definitions.chunks.MCLQ import MCLQ -from tools.map_extractor.definitions.chunks.MCVT import MCVT -from tools.map_extractor.definitions.enums.LiquidFlags import LiquidFlags +from tools.extractors.definitions.chunks.MCLQ import MCLQ +from tools.extractors.definitions.chunks.MCVT import MCVT +from tools.extractors.definitions.enums.LiquidFlags import LiquidFlags class TileInformation: diff --git a/tools/map_extractor/helpers/__init__.py b/tools/extractors/definitions/chunks/__init__.py similarity index 100% rename from tools/map_extractor/helpers/__init__.py rename to tools/extractors/definitions/chunks/__init__.py diff --git a/tools/map_extractor/definitions/enums/LiquidFlags.py b/tools/extractors/definitions/enums/LiquidFlags.py similarity index 100% rename from tools/map_extractor/definitions/enums/LiquidFlags.py rename to tools/extractors/definitions/enums/LiquidFlags.py diff --git a/tools/map_extractor/pydbclib/__init__.py b/tools/extractors/definitions/enums/__init__.py similarity index 100% rename from tools/map_extractor/pydbclib/__init__.py rename to tools/extractors/definitions/enums/__init__.py diff --git a/tools/map_extractor/definitions/reader/StreamReader.py b/tools/extractors/definitions/reader/StreamReader.py similarity index 100% rename from tools/map_extractor/definitions/reader/StreamReader.py rename to tools/extractors/definitions/reader/StreamReader.py diff --git a/tools/map_extractor/pympqlib/__init__.py b/tools/extractors/definitions/reader/__init__.py similarity index 100% rename from tools/map_extractor/pympqlib/__init__.py rename to tools/extractors/definitions/reader/__init__.py diff --git a/tools/map_extractor/helpers/Constants.py b/tools/extractors/helpers/Constants.py similarity index 100% rename from tools/map_extractor/helpers/Constants.py rename to tools/extractors/helpers/Constants.py diff --git a/tools/map_extractor/helpers/DataHolders.py b/tools/extractors/helpers/DataHolders.py similarity index 100% rename from tools/map_extractor/helpers/DataHolders.py rename to tools/extractors/helpers/DataHolders.py diff --git a/tools/map_extractor/helpers/HeightField.py b/tools/extractors/helpers/HeightField.py similarity index 98% rename from tools/map_extractor/helpers/HeightField.py rename to tools/extractors/helpers/HeightField.py index 45955f95f..7e17a72e5 100644 --- a/tools/map_extractor/helpers/HeightField.py +++ b/tools/extractors/helpers/HeightField.py @@ -1,7 +1,7 @@ from struct import pack from utils.ConfigManager import config from game.world.managers.abstractions.Vector import Vector -from tools.map_extractor.helpers.Constants import Constants +from tools.extractors.helpers.Constants import Constants from utils.Float16 import Float16 Z_RESOLUTION = 256 diff --git a/tools/map_extractor/helpers/LiquidAdtWriter.py b/tools/extractors/helpers/LiquidAdtWriter.py similarity index 95% rename from tools/map_extractor/helpers/LiquidAdtWriter.py rename to tools/extractors/helpers/LiquidAdtWriter.py index 9bf84dde6..ca96c0abb 100644 --- a/tools/map_extractor/helpers/LiquidAdtWriter.py +++ b/tools/extractors/helpers/LiquidAdtWriter.py @@ -1,6 +1,6 @@ from struct import pack -from tools.map_extractor.helpers.Constants import Constants -from tools.map_extractor.definitions.enums.LiquidFlags import LiquidFlags +from tools.extractors.helpers.Constants import Constants +from tools.extractors.definitions.enums.LiquidFlags import LiquidFlags from utils.ConfigManager import config from utils.Float16 import Float16 diff --git a/tools/extractors/helpers/__init__.py b/tools/extractors/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/map_extractor/pydbclib/DbcReader.py b/tools/extractors/pydbclib/DbcReader.py similarity index 97% rename from tools/map_extractor/pydbclib/DbcReader.py rename to tools/extractors/pydbclib/DbcReader.py index c03d0960b..5835e420e 100644 --- a/tools/map_extractor/pydbclib/DbcReader.py +++ b/tools/extractors/pydbclib/DbcReader.py @@ -3,7 +3,7 @@ from struct import unpack from typing import Optional from utils.Logger import Logger -from tools.map_extractor.pydbclib.structs.DbcHeader import DbcHeader +from tools.extractors.pydbclib.structs.DbcHeader import DbcHeader class DbcReader: diff --git a/tools/extractors/pydbclib/__init__.py b/tools/extractors/pydbclib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/map_extractor/pydbclib/helpers/VanillaAreaHelper.py b/tools/extractors/pydbclib/helpers/VanillaAreaHelper.py similarity index 100% rename from tools/map_extractor/pydbclib/helpers/VanillaAreaHelper.py rename to tools/extractors/pydbclib/helpers/VanillaAreaHelper.py diff --git a/tools/map_extractor/pydbclib/structs/AreaTable.py b/tools/extractors/pydbclib/structs/AreaTable.py similarity index 96% rename from tools/map_extractor/pydbclib/structs/AreaTable.py rename to tools/extractors/pydbclib/structs/AreaTable.py index baf9b2427..3fd2f4ad6 100644 --- a/tools/map_extractor/pydbclib/structs/AreaTable.py +++ b/tools/extractors/pydbclib/structs/AreaTable.py @@ -1,6 +1,6 @@ from struct import pack from dataclasses import dataclass -from tools.map_extractor.pydbclib.helpers.VanillaAreaHelper import VanillaAreaHelper +from tools.extractors.pydbclib.helpers.VanillaAreaHelper import VanillaAreaHelper @dataclass diff --git a/tools/map_extractor/pydbclib/structs/DbcHeader.py b/tools/extractors/pydbclib/structs/DbcHeader.py similarity index 100% rename from tools/map_extractor/pydbclib/structs/DbcHeader.py rename to tools/extractors/pydbclib/structs/DbcHeader.py diff --git a/tools/map_extractor/pydbclib/structs/Map.py b/tools/extractors/pydbclib/structs/Map.py similarity index 100% rename from tools/map_extractor/pydbclib/structs/Map.py rename to tools/extractors/pydbclib/structs/Map.py diff --git a/tools/map_extractor/pympqlib/MpqArchive.py b/tools/extractors/pympqlib/MpqArchive.py similarity index 96% rename from tools/map_extractor/pympqlib/MpqArchive.py rename to tools/extractors/pympqlib/MpqArchive.py index ad7b6e872..32ac61a41 100644 --- a/tools/map_extractor/pympqlib/MpqArchive.py +++ b/tools/extractors/pympqlib/MpqArchive.py @@ -2,10 +2,10 @@ from io import BytesIO from struct import unpack from utils.Logger import Logger -from tools.map_extractor.pympqlib.MpqHash import MpqHash -from tools.map_extractor.pympqlib.MpqEntry import MpqEntry -from tools.map_extractor.pympqlib.MpqHeader import MpqHeader -from tools.map_extractor.pympqlib.MpqReader import MpqReader +from tools.extractors.pympqlib.MpqHash import MpqHash +from tools.extractors.pympqlib.MpqEntry import MpqEntry +from tools.extractors.pympqlib.MpqHeader import MpqHeader +from tools.extractors.pympqlib.MpqReader import MpqReader class MpqArchive: diff --git a/tools/map_extractor/pympqlib/MpqEntry.py b/tools/extractors/pympqlib/MpqEntry.py similarity index 97% rename from tools/map_extractor/pympqlib/MpqEntry.py rename to tools/extractors/pympqlib/MpqEntry.py index 651d9d4e2..a9ab4384e 100644 --- a/tools/map_extractor/pympqlib/MpqEntry.py +++ b/tools/extractors/pympqlib/MpqEntry.py @@ -1,6 +1,6 @@ from io import BytesIO from struct import unpack -from tools.map_extractor.pympqlib.MpqFlags import MpqFlags +from tools.extractors.pympqlib.MpqFlags import MpqFlags class MpqEntry: diff --git a/tools/map_extractor/pympqlib/MpqFlags.py b/tools/extractors/pympqlib/MpqFlags.py similarity index 100% rename from tools/map_extractor/pympqlib/MpqFlags.py rename to tools/extractors/pympqlib/MpqFlags.py diff --git a/tools/map_extractor/pympqlib/MpqHash.py b/tools/extractors/pympqlib/MpqHash.py similarity index 100% rename from tools/map_extractor/pympqlib/MpqHash.py rename to tools/extractors/pympqlib/MpqHash.py diff --git a/tools/map_extractor/pympqlib/MpqHeader.py b/tools/extractors/pympqlib/MpqHeader.py similarity index 100% rename from tools/map_extractor/pympqlib/MpqHeader.py rename to tools/extractors/pympqlib/MpqHeader.py diff --git a/tools/map_extractor/pympqlib/MpqReader.py b/tools/extractors/pympqlib/MpqReader.py similarity index 100% rename from tools/map_extractor/pympqlib/MpqReader.py rename to tools/extractors/pympqlib/MpqReader.py diff --git a/tools/extractors/pympqlib/__init__.py b/tools/extractors/pympqlib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/utils/ConfigManager.py b/utils/ConfigManager.py index c10b178a2..2f57ea4bb 100644 --- a/utils/ConfigManager.py +++ b/utils/ConfigManager.py @@ -5,7 +5,7 @@ class ConfigManager: - EXPECTED_VERSION = 18 + EXPECTED_VERSION = 19 def __init__(self): self.config = None diff --git a/utils/GitUtils.py b/utils/GitUtils.py index 298e152db..9c250b345 100644 --- a/utils/GitUtils.py +++ b/utils/GitUtils.py @@ -1,15 +1,55 @@ +import os +import sys +from io import BytesIO + +import requests +import zipfile from os import path +from utils.Logger import Logger from utils.PathManager import PathManager - class GitUtils: HEAD_FILE_NAME = 'HEAD' + CONFIG_FILE_NAME = 'config' + MAPBUILD_FILE_NAME = 'mapbuild' + PATHFIND_FILE_NAME = 'pathfind' + BASE_NAMIGATOR_URL = 'https://github.com/The-Alpha-Project/alpha-core/releases/download/Namigator/' + + @staticmethod + def check_download_namigator_bindings(): + try: + if os.name == 'nt': + ext = 'pyd' + os_prefix = 'win' + else: + ext = 'so' + os_prefix = 'nix' + + # TODO: lib naming which includes python version in order to check if we need to update/downgrade. + if (os.path.isfile('namigator/' + GitUtils.MAPBUILD_FILE_NAME + '.' + ext) + and os.path.isfile('namigator/' + GitUtils.PATHFIND_FILE_NAME + '.' + ext)): + Logger.info('[Namigator] Found python bindings.') + return True + + py_runtime = str(sys.version_info[0]) + '.' + str(sys.version_info[1]) + filename = 'namigator_' + os_prefix + '_' + py_runtime + '.zip' + download_url = GitUtils.BASE_NAMIGATOR_URL + filename + + zip_data = requests.get(download_url) + Logger.info('[Namigator] Attempting to download ' + download_url) + with zipfile.ZipFile(BytesIO(zip_data.content)) as zip_file: + zip_file.extractall('namigator') + Logger.info('[Namigator] Binding installed.') + + return True + except: + return False @staticmethod def get_head_path(): try: - with open(path.join(PathManager.get_git_path(), GitUtils.HEAD_FILE_NAME), 'r') as git_head_file: + with open(os.path.join(PathManager.get_git_path(), GitUtils.HEAD_FILE_NAME), 'r') as git_head_file: # Contains e.g. ref: ref/heads/master if on "master". git_head_data = str(git_head_file.read()) return git_head_data.split(' ')[1].strip() diff --git a/utils/Srp6.py b/utils/Srp6.py new file mode 100644 index 000000000..bf7ff1c37 --- /dev/null +++ b/utils/Srp6.py @@ -0,0 +1,135 @@ +import os, hashlib + +''' +N Large safe prime +g Generator +k K value +''' + +zero = bytes([0, 0, 0, 0]) +SHA1 = hashlib.sha1 + +class Srp6: + g = 7 + g_bytes = g.to_bytes(1, byteorder='little') + N = 0x894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7 + N_Bytes = N.to_bytes(32, byteorder='little') + k = 3 + xorNg = bytes([ + 221, 123, 176, 58, 56, 172, 115, 17, 3, 152, 124, + 90, 80, 111, 202, 150, 108, 123, 194, 167, + ]) + + @staticmethod + def calculate_x(username:str, password:str, salt:bytes) -> bytes: + """ + x = SHA1( salt | SHA1( username | : | password )) + """ + interim = SHA1((username.upper() + ':' + password.upper()).encode()).digest() + x = SHA1(salt + interim).digest() + return x + + @staticmethod + def generate_salt(): + return os.urandom(32) + + @staticmethod + def calculate_password_verifier(username:str, password:str, salt:bytes) -> bytes: + """ + password_verifier = g^x % N + """ + x = int.from_bytes(Srp6.calculate_x(username, password, salt), byteorder='little') + v = pow(Srp6.g, x, Srp6.N) + return int.to_bytes(v, 32, 'little') + + @staticmethod + def calculate_server_public_key(password_verifier:bytes, server_private_key:bytes) -> bytes: + """ + server_public_key = (k * password_verifier + (g^server_private_key % N)) % N + """ + password_verifier = int.from_bytes(password_verifier, byteorder='little') + server_private_key = int.from_bytes(server_private_key, byteorder='little') + server_public_key = (Srp6.k * password_verifier + pow(Srp6.g, server_private_key, Srp6.N)) % Srp6.N + assert server_public_key % Srp6.N != 0 + return int.to_bytes(server_public_key, 32, 'little') + + @staticmethod + def calculate_client_s_key(client_private_key:bytes, server_public_key:bytes, x:bytes, u:bytes) ->bytes: + """ + s_key = (server_public_key - (k * (g^x % N)))^(client_private_key + u * x) % N + """ + client_private_key = int.from_bytes(client_private_key, byteorder='little') + server_public_key = int.from_bytes(server_public_key, byteorder='little') + x = int.from_bytes(x, byteorder='little') + u = int.from_bytes(u, byteorder='little') + s_key = pow((server_public_key - Srp6.k * pow(Srp6.g, x, Srp6.N)), (client_private_key + u * x), Srp6.N) + return int.to_bytes(s_key, 32, 'little') + + @staticmethod + def calculate_server_s_key(client_public_key, password_verifier, u, server_private_key) -> bytes: + """ + s_key = (client_public_key * (password_verifier^u % N))^server_private_key % N, + """ + client_public_key = int.from_bytes(client_public_key, byteorder='little') + password_verifier = int.from_bytes(password_verifier, byteorder='little') + u = int.from_bytes(u, byteorder='little') + server_private_key = int.from_bytes(server_private_key, byteorder='little') + s_key = pow((client_public_key * pow(password_verifier, u, Srp6.N)), server_private_key, Srp6.N) + return int.to_bytes(s_key, 32, 'little') + + @staticmethod + def calculate_u(client_public_key:bytes, server_public_key:bytes) -> bytes: + """ + u = SHA1( client_public_key | server_public_key ) + """ + u = SHA1(client_public_key+ server_public_key).digest() + return u + + @staticmethod + def calculate_interleaved(s_key:bytes) -> bytes: + """ + session key + """ + while s_key[0] == 0: + s_key = s_key[2:] + e = s_key[0::2] + f = s_key[1::2] + g = SHA1(e).digest() + h = SHA1(f).digest() + session_key = bytes(x for y in zip(g, h) for x in y) + return session_key + + @staticmethod + def calculate_server_proof(client_public_key:bytes, m1:bytes, session_key:bytes) -> bytes: + """ + m2 = SHA1(client_public_key | m1 | session_key) + """ + m2 = SHA1(client_public_key + m1 + session_key).digest() + return m2 + + @staticmethod + def calculate_client_proof(x:bytes, username:str, session_key:bytes, client_public_key:bytes, + server_public_key:bytes, salt:bytes) -> bytes: + """ + m1 = SHA1( x | SHA1(username) | salt | client_public_key | server_public_key | session_key ) + """ + u = SHA1(username.upper().encode()).digest() + m1 = SHA1(x + u + salt + client_public_key + server_public_key + session_key).digest() + return m1 + + @staticmethod + def calculate_client_public_key(a:bytes) -> bytes: + """ + client_public_key = generator^private_client_key % N + """ + client_private_key = int.from_bytes(a, byteorder='little') + client_public_key = pow(Srp6.g, client_private_key, Srp6.N) + assert client_public_key % Srp6.N != 0 + return int.to_bytes(client_public_key, 32, 'little') + + @staticmethod + def calculate_world_server_proof(username:str, client_seed:bytes, server_seed:bytes, session_key:bytes) -> bytes: + """ + SHA1( username | 0 | client_seed | server_seed | session_key ) + """ + return SHA1(username.upper().encode() + zero + client_seed + zero + server_seed + session_key).digest() diff --git a/utils/constants/AuthCodes.py b/utils/constants/AuthCodes.py index fee29834f..833a8a83e 100644 --- a/utils/constants/AuthCodes.py +++ b/utils/constants/AuthCodes.py @@ -1,6 +1,17 @@ from enum import IntEnum +class Srp6Operation(IntEnum): + MessageChallenge = 51 + MessageProof = 52 + MessageReproof = 53 + + +class Srp6ResponseType(IntEnum): + AuthChallenge = 0 + AuthProof = 1 + + class AuthCode(IntEnum): AUTH_OK = 0x0C AUTH_FAILED = 0x0D