From e0d6547c5aeb0be8d4ba9a92e06f1be1a3589080 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Fri, 27 Dec 2024 22:08:02 -0600 Subject: [PATCH 01/46] Ongoing Srp6 --- database/realm/RealmDatabaseManager.py | 17 +- database/realm/RealmModels.py | 3 + game/login/LoginManager.py | 48 ++ game/login/LoginSessionStateHandler.py | 98 ++++ game/login/__init__.py | 0 game/realm/RealmManager.py | 4 +- game/world/opcode_handling/Definitions.py | 2 + .../handlers/interface/AuthSessionHandler.py | 21 +- main.py | 13 +- utils/Srp6.py | 493 ++++++++++++++++++ utils/constants/MiscCodes.py | 5 + 11 files changed, 696 insertions(+), 8 deletions(-) create mode 100644 game/login/LoginManager.py create mode 100644 game/login/LoginSessionStateHandler.py create mode 100644 game/login/__init__.py create mode 100644 utils/Srp6.py diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index ace88a432..20e219dd2 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -8,8 +8,7 @@ from game.realm.AccountManager import AccountManager from utils.ConfigManager import * from utils.constants.ItemCodes import InventorySlots -from utils.constants.MiscCodes import HighGuid - +from utils.constants.MiscCodes import HighGuid, AuthType DB_USER = os.getenv('MYSQL_USERNAME', config.Database.Connection.username) DB_PASSWORD = os.getenv('MYSQL_PASSWORD', config.Database.Connection.password) @@ -34,6 +33,13 @@ def realm_get_list(): # Account- + @staticmethod + def account_get(username): + realm_db_session = SessionHolder() + account = realm_db_session.query(Account).filter_by(name=username).first() + realm_db_session.close() + return account + @staticmethod def account_try_login(username, password, ip): realm_db_session = SessionHolder() @@ -56,10 +62,13 @@ 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="0", verifier="0", auth_method=AuthType.SHA256): 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, + auth_method=auth_method) realm_db_session.add(account) realm_db_session.flush() realm_db_session.commit() diff --git a/database/realm/RealmModels.py b/database/realm/RealmModels.py index 5e87ad544..a1629ef45 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) + auth_method = Column(TINYINT(4), nullable=False) class AppliedUpdate(Base): diff --git a/game/login/LoginManager.py b/game/login/LoginManager.py new file mode 100644 index 000000000..aa72d20a3 --- /dev/null +++ b/game/login/LoginManager.py @@ -0,0 +1,48 @@ +import socket +import threading +import traceback + +from game.login.LoginSessionStateHandler import LoginSessionStateHandler +from utils.ConfigManager import config +from utils.Logger import Logger + + +class LoginManager: + @staticmethod + def build_socket(address, port): + 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(2) + return socket_ + + @staticmethod + def start_login(running, login_server_ready): + login_host = config.Server.Connection.Login.host + login_port = config.Server.Connection.Login.port + with LoginManager.build_socket(login_host, login_port) 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) + 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: + 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/game/login/__init__.py b/game/login/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/game/realm/RealmManager.py b/game/realm/RealmManager.py index d4ed63e3c..9b112e4b9 100644 --- a/game/realm/RealmManager.py +++ b/game/realm/RealmManager.py @@ -77,7 +77,7 @@ def start_realm(running, realm_server_ready): 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]}') + Logger.success(f'Realm server started, listening on {real_binding[0]}:{real_binding[1]}') realm_server_ready.value = 1 try: @@ -94,7 +94,7 @@ def start_realm(running, realm_server_ready): except KeyboardInterrupt: pass - Logger.info("Login server turned off.") + Logger.info("Realm server turned off.") @staticmethod def start_proxy(running, proxy_server_ready): diff --git a/game/world/opcode_handling/Definitions.py b/game/world/opcode_handling/Definitions.py index 2138a413a..f45237dbe 100644 --- a/game/world/opcode_handling/Definitions.py +++ b/game/world/opcode_handling/Definitions.py @@ -171,6 +171,8 @@ from utils.constants.OpCodes import OpCode HANDLER_DEFINITIONS = { + # Auth + OpCode.CMSG_AUTH_SRP6_BEGIN: AuthSessionHandler.handle_srp6_begin, 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..f249b319b 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -2,11 +2,30 @@ 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_srp6_begin(world_session, reader): + region, language, user_length = unpack( + ' bytes: + """ + x = SHA1( s | SHA1( U | : | p )) + """ + interim = SHA1((U.upper() + ':' + p.upper()).encode()).digest() + x = SHA1(s + interim).digest() + return x + + @staticmethod + def calculate_password_verifier(U:str, p:str, s:bytes) -> bytes: + """ + v = g^x % N + """ + x = int.from_bytes(Srp6.calculate_x(U, p, s), byteorder='little') + v = pow(g, x, N) + return int.to_bytes(v, 32, 'little') + + @staticmethod + def calculate_server_public_key(v:bytes, b:bytes) -> bytes: + """ + B = (k * v + (g^b % N)) % N + """ + v = int.from_bytes(v, byteorder='little') + b = int.from_bytes(b, byteorder='little') + B = (k * v + pow(g, b, N)) % N + assert B % N != 0 + return int.to_bytes(B, 32, 'little') + + @staticmethod + def calculate_client_S_key(a:bytes, B:bytes, x:bytes, u:bytes) ->bytes: + """ + S = (B - (k * (g^x % N)))^(a + u * x) % N + """ + a = int.from_bytes(a, byteorder='little') + B = int.from_bytes(B, byteorder='little') + x = int.from_bytes(x, byteorder='little') + u = int.from_bytes(u, byteorder='little') + S = pow((B - k * pow(g, x, N)), (a + u * x), N) + return int.to_bytes(S, 32, 'little') + + @staticmethod + def calculate_server_S_key(A, v, u, b) -> bytes: + """ + S = (A * (v^u % N))^b % N, + """ + A = int.from_bytes(A, byteorder='little') + v = int.from_bytes(v, byteorder='little') + u = int.from_bytes(u, byteorder='little') + b = int.from_bytes(b, byteorder='little') + S = pow((A * pow(v, u, N)), b, N) + return int.to_bytes(S, 32, 'little') + + @staticmethod + def calculate_u(A:bytes, B:bytes) -> bytes: + """ + u = SHA1( A | B ) + """ + u = SHA1(A + B).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() + K = bytes(x for y in zip(G, H) for x in y) + return K + + @staticmethod + def calculate_server_proof(A:bytes, M1:bytes, K:bytes) -> bytes: + """ + M2 = SHA1(A | M1 | K) + """ + M2 = SHA1(A + M1 + K).digest() + return M2 + + @staticmethod + def calculate_xor_hash() -> bytes: + """ + SHA1(N) XOR SHA1(g) + """ + x1 = int.to_bytes(g, 1, 'little') + x2 = bytes.fromhex('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7')[::-1] + t1 = SHA1(x1).digest() + t2 = SHA1(x2).digest() + assert len(t1) == len(t2) == 20 + result = (c_ubyte * 20)() + for n in range(20): + result[n] = t1[n] ^ t2[n] + return bytes(result) + + @staticmethod + def calculate_client_proof(X:bytes, U:str, K:bytes, A:bytes, B:bytes, s:bytes) -> bytes: + """ + M1 = SHA1( X | SHA1(U) | s | A | B | K ) + """ + U = SHA1(U.upper().encode()).digest() + M1 = SHA1(X + U + s + A + B + K).digest() + return M1 + + @staticmethod + def calculate_client_public_key(a:bytes) -> bytes: + ''' + A = g^a % N + ''' + a = int.from_bytes(a, byteorder='little') + A = pow(g, a, N) + assert A % N != 0 + return int.to_bytes(A, 32, 'little') + + @staticmethod + def calculate_reconnect_proof(username:str, client_data:bytes, server_data:bytes, session_key:bytes) -> bytes: + ''' + SHA1( username | client_data | server_data | session_key ) + ''' + return SHA1(username.upper().encode() + client_data + server_data + session_key).digest() + + @staticmethod + def encrypt(data:bytes, session_key:bytes) -> bytes: + ''' + E = (x ^ S) + L + ''' + index = 0 + last_value = 0 + size = len(data) + result = (c_ubyte * size)() + session_key_length = len(session_key) + for n in range(size): + unencrypted = data[n] + encrypted = (unencrypted ^ session_key[index]) + last_value + index = (index + 1) % session_key_length + last_value = encrypted + result[n] = encrypted + return bytes(result) + + @staticmethod + def decrypt(data:bytes, session_key:bytes) -> bytes: + ''' + x = (E - L) ^ S + ''' + index = 0 + last_value = 0 + size = len(data) + result = (c_ubyte * size)() + session_key_length = len(session_key) + for n in range(size): + encrypted = data[n] + unencrypted = (encrypted - last_value) ^ session_key[index] + index = (index + 1) % session_key_length + last_value = encrypted + result[n] = unencrypted + return bytes(result) + + @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 + server_seed + session_key).digest() + + + + # # bytes.fromhex()[::-1] == big -> little + # + # # test1 + # h1 = SHA1('test'.encode()).hexdigest() + # assert h1 == 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3' + # h2 = SHA1(b'\x53\x51').hexdigest() + # assert h2 == '0c3d7a19ac7c627290bf031ec3df76277b0f7f75' + # + # + # # test2 + # salt = bytes.fromhex('AFE5D28E925DBB3DAFED5D91ACA0928940E8FBFEF2D2A3CC154ADA0FE6ABEF6F')[::-1] + # expected = bytes.fromhex('21B4153B0A938D0A69D28F2690CC3F79A99A13C40CACB525B3B79D4201EB33FF')[::-1] + # #------------------------- + # U = 'LF2BGFQIFQ3HZ1ZF' + # p = 'MVRVMUJFWRA0IBVK' + # s = salt + # v = calculate_password_verifier(U, p, s) + # assert v == expected + # + # + # # test3 + # salt = bytes.fromhex('CAC94AF32D817BA64B13F18FDEDEF92AD4ED7EF7AB0E19E9F2AE13C828AEAF57')[::-1] + # expected = bytes.fromhex('D927E98BE3E9AF84FDC99DE9034F8E70ED7E90D6')[::-1] + # #------------------------- + # U = 'USERNAME123' + # p = 'PASSWORD123' + # s = salt + # x = calculate_x(U, p, s) + # assert expected == x + # + # + # # test4 + # expected = bytes.fromhex('E2F9A0F1E824006C98DA753448E743F7DAA1EAA1')[::-1] + # #------------------------- + # U = '00XD0QOSA9L8KMXC' + # p = '43R4Z35TKBKFW8JI' + # s = salt + # x = calculate_x(U, p, s) + # assert expected == x + # + # + # # test5 + # password_verifier = bytes.fromhex('870A98A3DA8CCAFE6B2F4B0C43A022A0C6CEF4374BA4A50CEBF3FACA60237DC4')[::-1] + # server_private_key = bytes.fromhex('ACDCB7CB1DE67DB1D5E0A37DAE80068BCCE062AE0EDA0CBEADF560BCDAE6D6B9')[::-1] + # expected = bytes.fromhex('85A204C987B68764FA69C523E32B940D1E1822B9E0F134FDC5086B1408A2BB43')[::-1] + # #------------------------- + # v = password_verifier + # b = server_private_key + # B = calculate_server_public_key(v, b) + # assert expected == B + # + # + # # test6 + # server_public_key = bytes.fromhex('E232D2C71AD1BF58DB9F7DBE51FFE271B6BDC61524F2E6B32ABFFFCAB09D09AB')[::-1] + # client_private_key = bytes.fromhex('FC3D610C4E2CEC5ECC7E47344D0ED81D2ACB938AB198EC7E2ED474AEFCC3ABD1')[::-1] + # x = bytes.fromhex('A4A7CB7DFBE00D26EE06F6B3DACC51E5779D7E8B')[::-1] + # u = bytes.fromhex('FDAFAEF0E77F0FE1BD2956CF1820D4BC964E5283')[::-1] + # expected = bytes.fromhex('3898DF5193EA6AA8111524A253DB480A51EA6160D1E41BC4B662420299B4A435')[::-1] + # #------------------------- + # a = client_private_key + # B = server_public_key + # S = calculate_client_S_key(a, B, x, u) + # assert expected == S + # + # + # # test7 + # client_public_key = bytes.fromhex('51CCDDFACF7F960EDF5030F09F0B033C0D08DB1E43FCBA3A92ABB4BE3535D1DB')[::-1] + # password_verifier = bytes.fromhex('6FC7D4ACFCFFFDCF780EE9BBD17AE507FFCDF586F83B2C9AEE2198F195DB3AB5')[::-1] + # u = bytes.fromhex('F9CEDDD82E776BEDB1A94852A9A7FFA4FCADD5DE')[::-1] + # server_private_key = bytes.fromhex('A5DBBFCB4C7A1B7C3041CAC9DDBD36CD646F9FBABDAD66A019BCBB8FEDF2FAAE')[::-1] + # expected = bytes.fromhex('3503B289A60D6DD59EBD6FD88DF24836833433E39048ECAFF7E887313554F85C')[::-1] + # #------------------------- + # A = client_public_key + # v = password_verifier + # b = server_private_key + # S = calculate_server_S_key(A, v, u, b) + # assert expected == S + # + # + # # test8 + # client_public_key = bytes.fromhex('6FCEEEE7D40AAF0C7A08DFE1EFD3FCE80A152AA436CECB77FC06DAF9E9E5BDF3')[::-1] + # server_public_key = bytes.fromhex('F8CD769BDE603FC8F48B9BE7C5BEAAA7BD597ABDBDAC1AEFCACF0EE13443A3B9')[::-1] + # expected = bytes.fromhex('1309BD7851A1A505B95D6F60A8D884133458D24E')[::-1] + # #------------------------- + # A = client_public_key + # B = server_public_key + # u = calculate_u(A, B) + # assert expected == u + # + # + # # test9 + # s_key = bytes.fromhex('8F4CEBD60DFC34E5C007E51BD4F3A4FF2BC1D930E2D3EA770D8D3EEDFF2DCCFC') + # expected = bytes.fromhex('EE144E1AE08DAC891AB63ABC42BF89738003343422E6B58131BEE4C3087A7027E55A7216D18D556C') + # #------------------------- + # session_key = calculate_interleaved(s_key) + # assert expected == session_key + # + # + # # test10 + # client_public_key = bytes.fromhex('BFD1AC65C8DAAAD88BF9DFF9AF8D1DCDF11DFD0C7E398EDCDF5DBBD08EFB39D3')[::-1] + # client_proof = bytes.fromhex('7EBBC190D9AB2DC0CD891372CB30DF1ED35CDA1E')[::-1] + # session_key = bytes.fromhex('9382b5e82c16e1105b8e8e88a99118811d88170fad6e8b35f236dbebbcc9c99bcab6cc9f8fe67648') + # expected = bytes.fromhex('269E3A3EF5DCD15944F043513BDA20D20FEBA2E0')[::-1] + # #------------------------- + # A = client_public_key + # M1 = client_proof + # K = session_key + # M2 = calculate_server_proof(A, M1, K) + # assert expected == M2 + # + # + # # test11 + # #------------------------- + # X = calculate_xor_hash() + # assert xorNg == X + # + # + # # test12 + # username = '7WG6SHZL33JMGPO4' + # session_key = bytes.fromhex('77a4d39cf9c0bf373ef870bd2941c339c575fdd1cbaa31c919ea7bd5023267d303e20fec9a9c402f') + # client_public_key = bytes.fromhex('0095FE039AFE5E1BADE9AC0CAEC3CB73D2D08BBF4CA8ADDBCDF0CE709ED5103F')[::-1] + # server_public_key = bytes.fromhex('00B0C41F58CCE894CFB816FA72CA344C9FE2ED7CE799452ADBA7ABDCD26EAE75')[::-1] + # salt = bytes.fromhex('00a4a09e0b5aca438b8cd837d0816ca26043dbd1eaef138eef72dcf3f696d03d')[::-1] + # expected = bytes.fromhex('7D07022B4064CCE633D679F61C6B212B6F8BC5C3')[::-1] + # #------------------------- + # U = username + # K = session_key + # A = client_public_key + # B = server_public_key + # s = salt + # M1 = calculate_client_proof(xorNg, U, K, A, B, s) + # assert expected == M1 + # + # + # # test13 + # client_private_key = bytes.fromhex('A47DD4CD70DA1B0EF7E1FA8C02DE68AF0CEFCC77ACA287FBC3ADCDE0E7B78FE7')[::-1] + # expected = bytes.fromhex('7186DF27C1A309B5B26E293CD00ADD01E7037E09116089F26E810FD2D962BC42')[::-1] + # #------------------------- + # a = client_private_key + # A = calculate_client_public_key(a) + # assert expected == A + # + # + # # test14 + # username = 'GXJ8M6VDUAC0JL9W' + # client_data = bytes.fromhex('DD801B2FBCF4F7ABC6023EFAAF6A9AEA') + # server_data = bytes.fromhex('0D27763BDEEF92CB273B7BC4EE72D0EC') + # session_key = bytes.fromhex('6A0E7B35C70C70DA142D57BF49FD25D84CCEE3D21CC1A10AD71323FB34F45F3006D606F1F39A6BB9') + # expected = bytes.fromhex('D94CE2B08B7FAC0919D7D5419D78CABFA372B6A9') + # #------------------------- + # reconnect = calculate_reconnect_proof(username, client_data, server_data, session_key) + # assert expected == reconnect + # + # + # # test15 + # session_key = bytes.fromhex('2EFEE7B0C177EBBDFF6676C56EFC2339BE9CAD14BF8B54BB5A86FBF81F6D424AA23CC9A3149FB175') + # data = bytes.fromhex('3d9ae196ef4f5be4df9ea8b9f4dd95fe68fe58b653cf1c2dbeaa0be167db9b27df32fd230f2eab9bd7e9b2f3fbf335d381ca') + # expected = bytes.fromhex('13777da3d109b912322a08841e3ff5bc92f4e98b77bb03997da999b22ae0b926a3b1e56580314b3932499ee11b9f7deb6915') + # #------------------------- + # encrypted_data = encrypt(data, session_key) + # assert expected == encrypted_data + # + # + # # test16 + # session_key = bytes.fromhex('2EFEE7B0C177EBBDFF6676C56EFC2339BE9CAD14BF8B54BB5A86FBF81F6D424AA23CC9A3149FB175') + # data = bytes.fromhex('3d9ae196ef4f5be4df9ea8b9f4dd95fe68fe58b653cf1c2dbeaa0be167db9b27df32fd230f2eab9bd7e9b2f3fbf335d381ca') + # expected = bytes.fromhex('13a3a0059817e73404d97cd455159b50d40af74a22f719aacb6a9a2e991982c61a6f0285f880cc8512ec2ef1c98fa923512f') + # #------------------------- + # unencrypted_data = decrypt(data, session_key) + # assert expected == unencrypted_data + # + # + # # test17 + # username = 'HQO7EWULX09Z4RE4' + # session_key = bytes.fromhex('77295B4E6745E8833293E07650252D635D5E4B14D2A9DA4FB1AE22FB00131E42C2B2EE7BF0D4D185')[::-1] + # server_seed = bytes.fromhex('2d0a01e2') + # client_seed = bytes.fromhex('a2ba5fb2') + # expected = bytes.fromhex('b26af9256f4bd20f0f11e2c786710542b92115bb') + # #------------------------- + # world_server_proof = calculate_world_server_proof(username, client_seed, server_seed, session_key) + # assert expected == world_server_proof + # + # + # + # # final test + # #------------------------- + # username = 'Mario' + # password = '5#BB-:*!skTu' + # + # # 1. create account, save s and v to database + # # salt + # s = os.urandom(32) + # # password verified + # v = calculate_password_verifier(username, password, s) + # + # + # # 2. [LogonChallenge] client -> LS: username + # + # + # # 3. [LogonChallenge] LS -> client: B, s, N, g + # # read s and v from database by username + # # server private key + # b = os.urandom(32) + # # server public key + # B = calculate_server_public_key(v, b) + # + # + # # 4. [LogonProof] client -> LS: A, M1 + # # client private key + # a = os.urandom(32) + # # client public key + # A = calculate_client_public_key(a) + # # client S key + # x = calculate_x(username, password, s) + # u = calculate_u(A, B) + # c_S = calculate_client_S_key(a, B, x, u) + # # client session key + # c_K = calculate_interleaved(c_S) + # # client proof + # c_M1 = calculate_client_proof(xorNg, username, c_K, A, B, s) + # + # + # # 5. [LogonProof] LS -> client: M2 + # # server S key + # u = calculate_u(A, B) + # s_S = calculate_server_S_key(A, v, u, b) + # # server session key + # s_K = calculate_interleaved(s_S) + # # check M + # s_M1 = calculate_client_proof(xorNg, username, s_K, A, B, s) + # # authenticated + # assert c_M1 == s_M1 + # # server proof + # s_M2 = calculate_server_proof(A, s_M1, s_K) + # + # + # # 6. [RealmList] ... + # c_M2 = calculate_server_proof(A, c_M1, c_K) + # # authenticated + # assert c_M2 == s_M2 + # + # + # # 7. [ReconnectChallenge] client -> LS: username + # + # + # # 8. [ReconnectChallenge] LS -> client: server_data + # # checks for an existing session with the username + # server_data = os.urandom(16) + # + # + # # 9. [ReconnectProof] client -> LS: client_data, client_proof + # client_data = os.urandom(16) + # client_proof = calculate_reconnect_proof(username, client_data, server_data, c_K) + # + # + # # 10. [ReconnectProof] ... + # server_proof = calculate_reconnect_proof(username, client_data, server_data, s_K) + # # authenticated + # assert server_proof == client_proof + # + # + # # 11. client connects to the WS + # + # + # # 12. [AuthChallenge] WS -> client: server_seed + # server_seed = os.urandom(4) + # + # + # # 13. [AuthSession] client -> server: client_seed, client_auth + # client_seed = os.urandom(4) + # client_auth = calculate_world_server_proof(username, client_seed, server_seed, c_K) + # + # + # # 14. [AuthSession] server -> client: encrypt_msg + # server_auth = calculate_world_server_proof(username, client_seed, server_seed, s_K) + # # authenticated + # assert client_auth == server_auth + # # send data + # server_msg = 'WeLcOmE oNbOaRd'.encode() + # encrypt_msg = encrypt(server_msg, s_K) + # + # + # # 15. [SessionMsg] client + # client_msg = decrypt(encrypt_msg, c_K) + # # verify + # assert server_msg == client_msg \ No newline at end of file diff --git a/utils/constants/MiscCodes.py b/utils/constants/MiscCodes.py index 786ebf4cf..a0c3c5098 100644 --- a/utils/constants/MiscCodes.py +++ b/utils/constants/MiscCodes.py @@ -1070,6 +1070,11 @@ class PoolType(IntEnum): GAMEOBJECT = 1 +class AuthType(IntEnum): + SHA256 = 0 + SRP6 = 1 + + class MapsNoNavs(IntEnum): @classmethod From 16a16888906ad8879a3773fd8fbd4dfa1e7a50ce Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:09:41 -0600 Subject: [PATCH 02/46] Ongoing Srp6 --- database/realm/RealmDatabaseManager.py | 6 +-- database/realm/RealmModels.py | 1 - etc/databases/realm/updates/updates.sql | 9 ++++ game/world/managers/CommandManager.py | 34 ++++++++++++ .../handlers/interface/AuthSessionHandler.py | 33 ++++++++---- main.py | 54 +++++++------------ network/packet/PacketWriter.py | 4 ++ 7 files changed, 91 insertions(+), 50 deletions(-) diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index 20e219dd2..b0406e9d5 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -62,13 +62,13 @@ def account_try_login(username, password, ip): return status, account_mgr @staticmethod - def account_create(username, password, ip, salt="0", verifier="0", auth_method=AuthType.SHA256): + 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), salt=salt, - verifier=verifier, - auth_method=auth_method) + verifier=verifier + ) realm_db_session.add(account) realm_db_session.flush() realm_db_session.commit() diff --git a/database/realm/RealmModels.py b/database/realm/RealmModels.py index a1629ef45..2bc7dc5a4 100644 --- a/database/realm/RealmModels.py +++ b/database/realm/RealmModels.py @@ -18,7 +18,6 @@ class Account(Base): gmlevel = Column(TINYINT(4), nullable=False, server_default=text("1")) salt = Column(String(256), nullable=False) verifier = Column(String(256), nullable=False) - auth_method = Column(TINYINT(4), nullable=False) class AppliedUpdate(Base): diff --git a/etc/databases/realm/updates/updates.sql b/etc/databases/realm/updates/updates.sql index 1b0dd08c9..569d0e10b 100644 --- a/etc/databases/realm/updates/updates.sql +++ b/etc/databases/realm/updates/updates.sql @@ -241,5 +241,14 @@ 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`, + + insert into applied_updates values ('100120251'); + end if; + end $ delimiter ; diff --git a/game/world/managers/CommandManager.py b/game/world/managers/CommandManager.py index 7e4979c4e..c919fe3ca 100644 --- a/game/world/managers/CommandManager.py +++ b/game/world/managers/CommandManager.py @@ -1,3 +1,5 @@ +import hashlib +import os from datetime import datetime, timedelta from os import path from pathlib import Path @@ -14,6 +16,7 @@ from game.world.managers.objects.units.creature.CreatureBuilder import CreatureBuilder from utils.ConfigManager import config from utils.GitUtils import GitUtils +from utils.Srp6 import Srp6 from utils.TextUtils import GameTextFormatter from utils.constants.MiscCodes import UnitDynamicTypes, MoveFlags from utils.constants.SpellCodes import SpellEffects, SpellTargetMask @@ -31,6 +34,18 @@ class CommandManager(object): DEV_LOC_LOG_FULL_PATH = path.join(DEV_LOG_PATH, DEV_LOC_LOG_FILE_NAME) + @staticmethod + def handle_conole_command(command_msg): + terminator_index = command_msg.find(' ') if ' ' in command_msg else len(command_msg) + command = command_msg[0:terminator_index].strip() + args = command_msg[terminator_index:].strip() + + if command in CONSOLE_COMMAND_DEFINITIONS: + command_func = CONSOLE_COMMAND_DEFINITIONS[command][0] + return command_func(args) + + return -1, 'Invalid command' + @staticmethod def handle_command(world_session, command_msg): terminator_index = command_msg.find(' ') if ' ' in command_msg else len(command_msg) @@ -1094,6 +1109,25 @@ def gmtag(world_session, args): return 0, f' 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/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index f249b319b..09a31b46e 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -17,12 +17,18 @@ def handle_srp6_begin(world_session, reader): username = PacketReader.read_string(reader.data, 9, user_length - 1).strip() account = RealmDatabaseManager.account_get(username) - # TODO: Need proper packet structure here. - if not account or account.auth_method != AuthType.SRP6: - data = pack('H', len(data)) + data + @staticmethod def get_packet(opcode, data=b''): if data is None: From 93fab63cf35dee97006de7116bad4f8c79c6f829 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 11 Jan 2025 00:31:05 -0600 Subject: [PATCH 03/46] Ongoing Srp6 --- database/realm/RealmModels.py | 2 + game/login/Srp6Session.py | 4 ++ game/world/opcode_handling/Definitions.py | 1 + .../handlers/interface/AuthSessionHandler.py | 65 +++++++++++++++++-- 4 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 game/login/Srp6Session.py diff --git a/database/realm/RealmModels.py b/database/realm/RealmModels.py index 2bc7dc5a4..0aa28dd17 100644 --- a/database/realm/RealmModels.py +++ b/database/realm/RealmModels.py @@ -19,6 +19,8 @@ class Account(Base): salt = Column(String(256), nullable=False) verifier = Column(String(256), nullable=False) + srp6_session = None + class AppliedUpdate(Base): __tablename__ = 'applied_updates' diff --git a/game/login/Srp6Session.py b/game/login/Srp6Session.py new file mode 100644 index 000000000..389a371d3 --- /dev/null +++ b/game/login/Srp6Session.py @@ -0,0 +1,4 @@ +class Srp6Session: + def __init__(self, server_public_key, server_private_key): + self.server_public_key: bytes = server_public_key + self.server_private_key: bytes = server_private_key diff --git a/game/world/opcode_handling/Definitions.py b/game/world/opcode_handling/Definitions.py index f45237dbe..b32e717e4 100644 --- a/game/world/opcode_handling/Definitions.py +++ b/game/world/opcode_handling/Definitions.py @@ -173,6 +173,7 @@ 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 09a31b46e..cf41cd542 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -1,4 +1,5 @@ from database.realm.RealmDatabaseManager import * +from game.login.Srp6Session import Srp6Session from game.world.WorldSessionStateHandler import WorldSessionStateHandler from network.packet.PacketReader import * from network.packet.PacketWriter import * @@ -19,22 +20,68 @@ def handle_srp6_begin(world_session, reader): # Can't auto generate from here, we have no plain password. if not account: - data = pack('<1B', 21) + data = pack('<2B', 21, 0) world_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) return 0 + world_session.account_mgr = account + salt = bytes.fromhex(account.salt) verifier = bytes.fromhex(account.verifier) - private_key = os.urandom(32) - public_key = Srp6.calculate_server_public_key(verifier, private_key) + server_private_key = os.urandom(32) + server_public_key = Srp6.calculate_server_public_key(verifier, server_private_key) + account.srp6_session = Srp6Session(server_public_key, server_private_key) + g = 7 + N = 0x894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7 + + data = pack('<2B', 12, 0) + data += pack(' Date: Sat, 11 Jan 2025 00:42:47 -0600 Subject: [PATCH 04/46] Update AuthSessionHandler.py --- .../handlers/interface/AuthSessionHandler.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index cf41cd542..4d524d6ac 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -97,6 +97,21 @@ def handle(world_session, reader): # Through launcher (WoW.exe) if not username and not password: username = PacketReader.read_string(reader.data, 8) + # TODO: Figure how do we validate launcher authentication using the data below. + # CDataStore::Put(&resp, 478); - Opcode + # CDataStore::Put(&resp, 3368); - Version + # CDataStore::Put(&resp, this->m_loginData.m_loginServerID); - 0 + # CDataStore::PutString(&resp, this->m_loginData.m_account); - Username + # localChallenge = NTempest::CRandom::uint32_(&g_rndSeed); - Seed + # CDataStore::Put(&resp, localChallenge); + # SHA1_Update((const char *) & ctx, this->m_loginData.m_account, v6); + # SHA1_Update((const char *) & ctx, (char *) & msgId, 4u); + # SHA1_Update((const char *) & ctx, (char *) & localChallenge, 4u); + # SHA1_Update((const char *) & ctx, (char *) & loginServerID, 4u); + # SHA1_Update((const char *) & ctx, (char *) & challenge, 4u); + # SHA1_Update((const char *) & ctx, this->m_loginData.m_sessionKey, 0x28u); + # SHA1_Final((SHA1_CONTEXT *)localDigest, (int) & ctx); + # CDataStore::PutData( & resp, localDigest, 0x14u); - 20 byte digest. client_seed = unpack(' Date: Sat, 11 Jan 2025 19:03:10 -0600 Subject: [PATCH 05/46] Ongoing Srp6 --- database/realm/RealmDatabaseManager.py | 18 ++++++++ database/realm/RealmModels.py | 1 + etc/databases/realm/updates/updates.sql | 1 + .../handlers/interface/AuthSessionHandler.py | 44 ++++++++++++------- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index b0406e9d5..0378f28dc 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -76,6 +76,24 @@ def account_create(username, password, ip, salt, verifier): 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 0aa28dd17..2661a5dcf 100644 --- a/database/realm/RealmModels.py +++ b/database/realm/RealmModels.py @@ -18,6 +18,7 @@ class Account(Base): 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) srp6_session = None diff --git a/etc/databases/realm/updates/updates.sql b/etc/databases/realm/updates/updates.sql index 569d0e10b..acfad45b7 100644 --- a/etc/databases/realm/updates/updates.sql +++ b/etc/databases/realm/updates/updates.sql @@ -246,6 +246,7 @@ begin not atomic 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; diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index 4d524d6ac..ebce1718c 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -69,6 +69,11 @@ def handle_srp6_proof(world_session, reader): world_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) return 0 + if not RealmDatabaseManager.account_try_update_session_key(world_session.account_mgr.name, s_K.hex()): + data = pack('<2B', 22, 1) + world_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) + return 0 + # Server proof. s_M2 = Srp6.calculate_server_proof(client_public_key, s_M1, s_K) @@ -97,23 +102,28 @@ def handle(world_session, reader): # Through launcher (WoW.exe) if not username and not password: username = PacketReader.read_string(reader.data, 8) - # TODO: Figure how do we validate launcher authentication using the data below. - # CDataStore::Put(&resp, 478); - Opcode - # CDataStore::Put(&resp, 3368); - Version - # CDataStore::Put(&resp, this->m_loginData.m_loginServerID); - 0 - # CDataStore::PutString(&resp, this->m_loginData.m_account); - Username - # localChallenge = NTempest::CRandom::uint32_(&g_rndSeed); - Seed - # CDataStore::Put(&resp, localChallenge); - # SHA1_Update((const char *) & ctx, this->m_loginData.m_account, v6); - # SHA1_Update((const char *) & ctx, (char *) & msgId, 4u); - # SHA1_Update((const char *) & ctx, (char *) & localChallenge, 4u); - # SHA1_Update((const char *) & ctx, (char *) & loginServerID, 4u); - # SHA1_Update((const char *) & ctx, (char *) & challenge, 4u); - # SHA1_Update((const char *) & ctx, this->m_loginData.m_sessionKey, 0x28u); - # SHA1_Final((SHA1_CONTEXT *)localDigest, (int) & ctx); - # CDataStore::PutData( & resp, localDigest, 0x14u); - 20 byte digest. - client_seed = unpack('m_loginData.m_loginServerID); - 0 + # CDataStore::PutString(&resp, this->m_loginData.m_account); - Username + # localChallenge = NTempest::CRandom::uint32_(&g_rndSeed); - Seed + # CDataStore::Put(&resp, localChallenge); + # SHA1_Update((const char *) & ctx, this->m_loginData.m_account, v6); + # SHA1_Update((const char *) & ctx, (char *) & msgId, 4u); + # SHA1_Update((const char *) & ctx, (char *) & localChallenge, 4u); + # SHA1_Update((const char *) & ctx, (char *) & loginServerID, 4u); + # SHA1_Update((const char *) & ctx, (char *) & challenge, 4u); + # SHA1_Update((const char *) & ctx, this->m_loginData.m_sessionKey, 0x28u); + # SHA1_Final((SHA1_CONTEXT *)localDigest, (int) & ctx); + # CDataStore::PutData( & resp, localDigest, 0x14u); - 20 byte digest. + client_seed = reader.data[len(username) + 8:len(username) + 12] + client_digest = reader.data[len(username) + 12:-1] + server_seed = os.urandom(4) + server_auth = Srp6.calculate_world_server_proof(username, client_seed, server_seed, + bytes.fromhex(account_mgr.sessionkey)) if version != config.Server.Settings.supported_client: auth_code = AuthCode.AUTH_VERSION_MISMATCH From 43a0491e5c67719bee34474f8c55d1b928bb6f4c Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:21:01 -0600 Subject: [PATCH 06/46] Ongoing Srp6 --- game/world/WorldManager.py | 4 ++-- .../handlers/interface/AuthSessionHandler.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/game/world/WorldManager.py b/game/world/WorldManager.py index 07753816b..e99b39bcc 100644 --- a/game/world/WorldManager.py +++ b/game/world/WorldManager.py @@ -20,6 +20,7 @@ STARTUP_TIME = time() WORLD_ON = True +SERVER_SEED = os.urandom(4) MAX_PACKET_BYTES = 4096 @@ -143,10 +144,9 @@ def disconnect(self): # We handle auth_challenge before launching queue threads and anything else. def auth_challenge(self, sck): - data = pack(' Date: Sun, 12 Jan 2025 00:23:58 -0600 Subject: [PATCH 07/46] Ongoing Srp6 --- database/realm/RealmDatabaseManager.py | 9 +- database/realm/RealmModels.py | 2 - game/login/Srp6Session.py | 4 - game/realm/AccountManager.py | 53 +++++ .../handlers/interface/AuthSessionHandler.py | 182 ++++++++---------- utils/Srp6.py | 37 ++-- 6 files changed, 154 insertions(+), 133 deletions(-) delete mode 100644 game/login/Srp6Session.py diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index 0378f28dc..f0483dac3 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -36,18 +36,21 @@ def realm_get_list(): @staticmethod def account_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 + return account_mgr @staticmethod - def account_try_login(username, password, ip): + 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 account.password == password or (client_digest and client_digest == server_digest): status = 1 account.ip = ip account_mgr = AccountManager(account) diff --git a/database/realm/RealmModels.py b/database/realm/RealmModels.py index 2661a5dcf..a91817fed 100644 --- a/database/realm/RealmModels.py +++ b/database/realm/RealmModels.py @@ -20,8 +20,6 @@ class Account(Base): verifier = Column(String(256), nullable=False) sessionkey = Column(String(256), nullable=False) - srp6_session = None - class AppliedUpdate(Base): __tablename__ = 'applied_updates' diff --git a/game/login/Srp6Session.py b/game/login/Srp6Session.py deleted file mode 100644 index 389a371d3..000000000 --- a/game/login/Srp6Session.py +++ /dev/null @@ -1,4 +0,0 @@ -class Srp6Session: - def __init__(self, server_public_key, server_private_key): - self.server_public_key: bytes = server_public_key - self.server_private_key: bytes = server_private_key diff --git a/game/realm/AccountManager.py b/game/realm/AccountManager.py index de60ea3af..dea1607ed 100644 --- a/game/realm/AccountManager.py +++ b/game/realm/AccountManager.py @@ -1,3 +1,8 @@ +import os +from struct import pack + +from network.packet.PacketWriter import PacketWriter +from utils.Srp6 import Srp6 from utils.constants import CustomCodes @@ -5,6 +10,11 @@ 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 +27,46 @@ 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_S = Srp6.calculate_server_S_key(client_public_key, self.get_verifier_bytes(), u, self._server_private_key) + self._session_key = Srp6.calculate_interleaved(s_S) + 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', 12, 1) + data += s_M2 + data += pack(' bytes: + data = pack('<2B', 12, 0) + data += pack('m_loginData.m_loginServerID); - 0 - # CDataStore::PutString(&resp, this->m_loginData.m_account); - Username - # localChallenge = NTempest::CRandom::uint32_(&g_rndSeed); - Seed - # CDataStore::Put(&resp, localChallenge); - # SHA1_Update((const char *) & ctx, this->m_loginData.m_account, v6); - # SHA1_Update((const char *) & ctx, (char *) & msgId, 4u); - # SHA1_Update((const char *) & ctx, (char *) & localChallenge, 4u); - # SHA1_Update((const char *) & ctx, (char *) & loginServerID, 4u); - # SHA1_Update((const char *) & ctx, (char *) & challenge, 4u); - # SHA1_Update((const char *) & ctx, this->m_loginData.m_sessionKey, 0x28u); - # SHA1_Final((SHA1_CONTEXT *)localDigest, (int) & ctx); - # CDataStore::PutData( & resp, localDigest, 0x14u); - 20 byte digest. - client_seed = reader.data[len(username) + 8:len(username) + 12] - client_digest = reader.data[len(username) + 12:-1] - server_digest = Srp6.calculate_world_server_proof(username, client_seed, WorldManager.SERVER_SEED, - bytes.fromhex(account_mgr.sessionkey)) - # TODO: Does not match. - if client_digest == server_digest: - pass + AuthSessionHandler.send_result(world_session, auth_code) - if version != config.Server.Settings.supported_client: - auth_code = AuthCode.AUTH_VERSION_MISMATCH - - if username and password: - login_res, world_session.account_mgr = RealmDatabaseManager.account_try_login( - username, hashlib.sha256(password.encode('utf-8')).hexdigest(), - world_session.client_socket.getpeername()[0]) - if login_res == 0: - auth_code = AuthCode.AUTH_INCORRECT_PASSWORD - elif login_res == -1: - if config.Server.Settings.auto_create_accounts: - salt = os.urandom(32) - verifier = Srp6.calculate_password_verifier(username, password, salt) - world_session.account_mgr = ( - RealmDatabaseManager.account_create(username, - hashlib.sha256(password.encode('utf-8')).hexdigest(), - world_session.client_socket.getpeername()[0], - salt.hex(), - verifier.hex()) - ) - else: - auth_code = AuthCode.AUTH_UNKNOWN_ACCOUNT + return 0 if auth_code == AuthCode.AUTH_OK else -1 + @staticmethod + def send_result(world_session, auth_code): WorldSessionStateHandler.disconnect_old_session(world_session) WorldSessionStateHandler.add(world_session) - - data = pack(' bytes: @@ -45,13 +44,17 @@ def calculate_x(U:str, p:str, s:bytes) -> bytes: x = SHA1(s + interim).digest() return x + @staticmethod + def generate_salt(): + return os.urandom(32) + @staticmethod def calculate_password_verifier(U:str, p:str, s:bytes) -> bytes: """ v = g^x % N """ x = int.from_bytes(Srp6.calculate_x(U, p, s), byteorder='little') - v = pow(g, x, N) + v = pow(Srp6.g, x, Srp6.N) return int.to_bytes(v, 32, 'little') @staticmethod @@ -61,8 +64,8 @@ def calculate_server_public_key(v:bytes, b:bytes) -> bytes: """ v = int.from_bytes(v, byteorder='little') b = int.from_bytes(b, byteorder='little') - B = (k * v + pow(g, b, N)) % N - assert B % N != 0 + B = (Srp6.k * v + pow(Srp6.g, b, Srp6.N)) % Srp6.N + assert B % Srp6.N != 0 return int.to_bytes(B, 32, 'little') @staticmethod @@ -74,7 +77,7 @@ def calculate_client_S_key(a:bytes, B:bytes, x:bytes, u:bytes) ->bytes: B = int.from_bytes(B, byteorder='little') x = int.from_bytes(x, byteorder='little') u = int.from_bytes(u, byteorder='little') - S = pow((B - k * pow(g, x, N)), (a + u * x), N) + S = pow((B - Srp6.k * pow(Srp6.g, x, Srp6.N)), (a + u * x), Srp6.N) return int.to_bytes(S, 32, 'little') @staticmethod @@ -86,7 +89,7 @@ def calculate_server_S_key(A, v, u, b) -> bytes: v = int.from_bytes(v, byteorder='little') u = int.from_bytes(u, byteorder='little') b = int.from_bytes(b, byteorder='little') - S = pow((A * pow(v, u, N)), b, N) + S = pow((A * pow(v, u, Srp6.N)), b, Srp6.N) return int.to_bytes(S, 32, 'little') @staticmethod @@ -124,7 +127,7 @@ def calculate_xor_hash() -> bytes: """ SHA1(N) XOR SHA1(g) """ - x1 = int.to_bytes(g, 1, 'little') + x1 = int.to_bytes(Srp6.g, 1, 'little') x2 = bytes.fromhex('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7')[::-1] t1 = SHA1(x1).digest() t2 = SHA1(x2).digest() @@ -149,8 +152,8 @@ def calculate_client_public_key(a:bytes) -> bytes: A = g^a % N ''' a = int.from_bytes(a, byteorder='little') - A = pow(g, a, N) - assert A % N != 0 + A = pow(Srp6.g, a, Srp6.N) + assert A % Srp6.N != 0 return int.to_bytes(A, 32, 'little') @staticmethod @@ -201,7 +204,7 @@ def calculate_world_server_proof(username:str, client_seed:bytes, server_seed:by ''' SHA1( username | 0 | client_seed | server_seed | session_key ) ''' - return SHA1(username.upper().encode() + zero + client_seed + server_seed + session_key).digest() + return SHA1(username.upper().encode() + zero + client_seed + zero + server_seed + session_key).digest() From a5119ffce2c35c9bee5593b69ad414cd1dda6fd9 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:26:50 -0600 Subject: [PATCH 08/46] Update AuthSessionHandler.py --- .../opcode_handling/handlers/interface/AuthSessionHandler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index 24cf71590..cac8853c9 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -66,7 +66,6 @@ def handle(world_session, reader): auth_code = AuthCode.AUTH_UNKNOWN_ACCOUNT username = '' password = '' - client_seed = b'' client_digest = b'' server_digest = b'' From 06676156fa14f12c4519c7a6f92e577e96e13044 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:30:02 -0600 Subject: [PATCH 09/46] Update AuthSessionHandler.py --- .../opcode_handling/handlers/interface/AuthSessionHandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index cac8853c9..3a1cd08e9 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -43,12 +43,12 @@ def handle_srp6_proof(auth_session, reader): if not s_M1 == c_M1: data = pack('<2B', 22, 1) auth_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) - return 0 + return -1 if not auth_session.account_mgr.save_session_key(): data = pack('<2B', 22, 1) auth_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) - return 0 + return -1 # Send server proof, at this point client is authenticated. auth_session.client_socket.sendall(auth_session.account_mgr.get_srp6_server_proof_packet()) From 8c809e025793cacb927c59eaa0021aa31e9dab73 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:31:34 -0600 Subject: [PATCH 10/46] Update RealmDatabaseManager.py --- database/realm/RealmDatabaseManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index f0483dac3..e1c648d61 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -34,7 +34,7 @@ def realm_get_list(): # Account- @staticmethod - def account_get(username): + def account_try_get(username): realm_db_session = SessionHolder() account_mgr = None account = realm_db_session.query(Account).filter_by(name=username).first() From 6eab0d4c734911f8b332183455435eed240a6d31 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:31:36 -0600 Subject: [PATCH 11/46] Update AuthSessionHandler.py --- .../opcode_handling/handlers/interface/AuthSessionHandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index 3a1cd08e9..053da2fac 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -16,7 +16,7 @@ def handle_srp6_begin(auth_session, reader): ) username = PacketReader.read_string(reader.data, 9, user_length - 1).strip() - account_mgr = RealmDatabaseManager.account_get(username) + account_mgr = RealmDatabaseManager.account_try_get(username) # Can't auto generate from here, we have no plain password. if not account_mgr: @@ -83,7 +83,7 @@ def handle(world_session, reader): AuthSessionHandler.send_result(world_session, AuthCode.AUTH_UNKNOWN_ACCOUNT) return -1 - account_mgr = RealmDatabaseManager.account_get(username) + account_mgr = RealmDatabaseManager.account_try_get(username) # Can only auto generate accounts through old wow.ses which exposes plain password. if not account_mgr and config.Server.Settings.auto_create_accounts and password: salt = Srp6.generate_salt().hex() From 161cf0bdbac1ac274f625ffa591455223abf07da Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:36:15 -0600 Subject: [PATCH 12/46] Update MiscCodes.py --- utils/constants/MiscCodes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/utils/constants/MiscCodes.py b/utils/constants/MiscCodes.py index a0c3c5098..786ebf4cf 100644 --- a/utils/constants/MiscCodes.py +++ b/utils/constants/MiscCodes.py @@ -1070,11 +1070,6 @@ class PoolType(IntEnum): GAMEOBJECT = 1 -class AuthType(IntEnum): - SHA256 = 0 - SRP6 = 1 - - class MapsNoNavs(IntEnum): @classmethod From 344d2fb627c777ba9d49a3662d80fe9f3ab6687c Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:36:16 -0600 Subject: [PATCH 13/46] Update RealmDatabaseManager.py --- database/realm/RealmDatabaseManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index e1c648d61..8da5bd728 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -8,7 +8,7 @@ from game.realm.AccountManager import AccountManager from utils.ConfigManager import * from utils.constants.ItemCodes import InventorySlots -from utils.constants.MiscCodes import HighGuid, AuthType +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) From a4d97d9c667871f240d6e1a2a15fc2dc48f99202 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:38:44 -0600 Subject: [PATCH 14/46] Bump config version. --- etc/config/config.yml.dist | 6 +++++- utils/ConfigManager.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/etc/config/config.yml.dist b/etc/config/config.yml.dist index 3c06693a6..188fa0540 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,10 @@ Database: Server: Connection: # Change 0.0.0.0 for 127.0.0.1 if it doesn't work + Login: + host: 127.0.0.1 + port: 3724 + Realm: local_realm_id: 1 # id of the realm running on this machine (realmlist table) 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 From 807f7b4b90b23718f2980e60b943ee0f04a27e1c Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:01:34 -0600 Subject: [PATCH 15/46] Minor refactor. --- etc/config/config.yml.dist | 2 +- .../handlers/interface/AuthSessionHandler.py | 17 ++++++++++------- utils/constants/AuthCodes.py | 11 +++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/etc/config/config.yml.dist b/etc/config/config.yml.dist index 188fa0540..061614b7c 100644 --- a/etc/config/config.yml.dist +++ b/etc/config/config.yml.dist @@ -16,7 +16,7 @@ Database: Server: Connection: # Change 0.0.0.0 for 127.0.0.1 if it doesn't work Login: - host: 127.0.0.1 + host: 0.0.0.0 port: 3724 Realm: diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index 053da2fac..6a14953a3 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -20,13 +20,14 @@ def handle_srp6_begin(auth_session, reader): # Can't auto generate from here, we have no plain password. if not account_mgr: - data = pack('<2B', 21, 0) + data = pack('<2B', AuthCode.AUTH_UNKNOWN_ACCOUNT, Srp6ResponseType.AuthChallenge) auth_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) return -1 auth_session.account_mgr = account_mgr account_mgr.update_server_public_private_keys() - auth_session.client_socket.sendall(account_mgr.get_srp6_logon_challenge_packet()) + logon_challenge_packet = account_mgr.get_srp6_logon_challenge_packet() + auth_session.client_socket.sendall(logon_challenge_packet) return 0 @@ -36,22 +37,24 @@ def handle_srp6_proof(auth_session, reader): # Client proof. c_M1 = reader.data[32:52] - # Server proof. + # Client Server proof. s_M1 = auth_session.account_mgr.calculate_client_server_proof(client_public_key) # Invalid password. if not s_M1 == c_M1: - data = pack('<2B', 22, 1) + data = pack('<2B', AuthCode.AUTH_INCORRECT_PASSWORD, Srp6ResponseType.AuthProof) auth_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) return -1 if not auth_session.account_mgr.save_session_key(): - data = pack('<2B', 22, 1) + data = pack('<2B', AuthCode.AUTH_INCORRECT_PASSWORD, Srp6ResponseType.AuthProof) auth_session.client_socket.sendall(PacketWriter.get_srp6_packet(data)) return -1 # Send server proof, at this point client is authenticated. - auth_session.client_socket.sendall(auth_session.account_mgr.get_srp6_server_proof_packet()) + server_proof_packet = auth_session.account_mgr.get_srp6_server_proof_packet() + auth_session.client_socket.sendall(server_proof_packet) + auth_session.client_socket.close() return 0 @@ -125,4 +128,4 @@ def send_result(world_session, auth_code): WorldSessionStateHandler.disconnect_old_session(world_session) WorldSessionStateHandler.add(world_session) packet = PacketWriter.get_packet(OpCode.SMSG_AUTH_RESPONSE, pack(' Date: Mon, 13 Jan 2025 19:40:07 -0600 Subject: [PATCH 16/46] Update RealmDatabaseManager.py --- database/realm/RealmDatabaseManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index 8da5bd728..ebaf4a9dc 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -50,7 +50,7 @@ def account_try_login(username, password, ip, client_digest, server_digest): status = -1 account_mgr = None if account: - if account.password == password or (client_digest and client_digest == server_digest): + if (password and account.password == password) or (client_digest and client_digest == server_digest): status = 1 account.ip = ip account_mgr = AccountManager(account) From 44941ce82902b7e366c65beaf908bdfa5e2c2dc1 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Tue, 14 Jan 2025 01:15:03 -0600 Subject: [PATCH 17/46] Add dummy update server for launcher. - Requires Update.txt inside /Data: 127.0.0.1:9081 --- etc/config/config.yml.dist | 4 ++ game/login/LoginManager.py | 6 +-- game/update/UpdateManager.py | 48 ++++++++++++++++++++++++ game/update/UpdateSessionStateHandler.py | 19 ++++++++++ game/update/__init__.py | 0 main.py | 13 +++++++ 6 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 game/update/UpdateManager.py create mode 100644 game/update/UpdateSessionStateHandler.py create mode 100644 game/update/__init__.py diff --git a/etc/config/config.yml.dist b/etc/config/config.yml.dist index 061614b7c..476280764 100644 --- a/etc/config/config.yml.dist +++ b/etc/config/config.yml.dist @@ -15,6 +15,10 @@ 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 diff --git a/game/login/LoginManager.py b/game/login/LoginManager.py index aa72d20a3..b0384d655 100644 --- a/game/login/LoginManager.py +++ b/game/login/LoginManager.py @@ -35,9 +35,9 @@ def start_login(running, login_server_ready): try: client_socket, client_address = server_socket.accept() server_handler = LoginSessionStateHandler(client_socket, client_address) - world_session_thread = threading.Thread(target=server_handler.handle) - world_session_thread.daemon = True - world_session_thread.start() + 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: diff --git a/game/update/UpdateManager.py b/game/update/UpdateManager.py new file mode 100644 index 000000000..6799660fc --- /dev/null +++ b/game/update/UpdateManager.py @@ -0,0 +1,48 @@ +import socket +import threading +import traceback + +from game.update.UpdateSessionStateHandler import UpdateSessionStateHandler +from utils.ConfigManager import config +from utils.Logger import Logger + + +class UpdateManager: + @staticmethod + def build_socket(address, port): + 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(2) + return socket_ + + @staticmethod + def start_update(running, update_server_ready): + update_host = config.Server.Connection.Update.host + update_port = config.Server.Connection.Update.port + with UpdateManager.build_socket(update_host, update_port) as server_socket: + server_socket.listen() + real_binding = server_socket.getsockname() + Logger.success(f'Update server started, listening on {real_binding[0]}:{real_binding[1]}') + update_server_ready.value = 1 + + try: + while running.value: + try: + client_socket, client_address = server_socket.accept() + server_handler = UpdateSessionStateHandler(client_socket, client_address) + update_session_thread = threading.Thread(target=server_handler.handle) + update_session_thread.daemon = True + update_session_thread.start() + except socket.timeout: + pass # Non blocking. + except OSError: + Logger.warning(traceback.format_exc()) + except KeyboardInterrupt: + pass + + Logger.info("Update server turned off.") diff --git a/game/update/UpdateSessionStateHandler.py b/game/update/UpdateSessionStateHandler.py new file mode 100644 index 000000000..6262cdcd2 --- /dev/null +++ b/game/update/UpdateSessionStateHandler.py @@ -0,0 +1,19 @@ +import socket +MAX_PACKET_BYTES = 4096 + + +class UpdateSessionStateHandler: + def __init__(self, client_socket, client_address): + self.client_socket = client_socket + self.client_address = client_address + + # TODO: UpdateServer seems to use some kind of RSYNC protocol for files. + def handle(self): + self.disconnect() + + def disconnect(self): + try: + self.client_socket.shutdown(socket.SHUT_RDWR) + self.client_socket.close() + except OSError: + pass diff --git a/game/update/__init__.py b/game/update/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/main.py b/main.py index e1b0acbc9..b24ae85c3 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ from game.login.LoginManager import LoginManager from game.realm.RealmManager import RealmManager +from game.update.UpdateManager import UpdateManager from game.world import WorldManager from game.world.managers.CommandManager import CommandManager from game.world.managers.maps.MapManager import MapManager @@ -101,12 +102,18 @@ def wait_login_server(): sleep(1) +def wait_update_server(): + while not UPDATE_SERVER_READY.value and RUNNING.value: + sleep(1) + + CONSOLE_THREAD = None RUNNING = None WORLD_SERVER_READY = None REALM_SERVER_READY = None PROXY_SERVER_READY = None LOGIN_SERVER_READY = None +UPDATE_SERVER_READY = None ACTIVE_PROCESSES = [] @@ -145,6 +152,7 @@ def wait_login_server(): REALM_SERVER_READY = context.Value('i', 0) PROXY_SERVER_READY = context.Value('i', 0) LOGIN_SERVER_READY = context.Value('i', 0) + UPDATE_SERVER_READY = context.Value('i', 0) # Print active env vars. for env_var_name in EnvVars.EnvironmentalVariables.ACTIVE_ENV_VARS: @@ -174,6 +182,11 @@ def wait_login_server(): else: WORLD_SERVER_READY.value = 1 + # Update server. + ACTIVE_PROCESSES.append((context.Process(name='Update process', target=UpdateManager.start_update, + args=(RUNNING, UPDATE_SERVER_READY)), wait_update_server)) + + # SRP login server. ACTIVE_PROCESSES.append((context.Process(name='Login process', target=LoginManager.start_login, args=(RUNNING, LOGIN_SERVER_READY)), wait_login_server)) From 26991959e7128bc08ac4367fe491a28e9dff563e Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:02:52 -0600 Subject: [PATCH 18/46] Update RealmDatabaseManager.py --- database/realm/RealmDatabaseManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/realm/RealmDatabaseManager.py b/database/realm/RealmDatabaseManager.py index ebaf4a9dc..d5334650a 100644 --- a/database/realm/RealmDatabaseManager.py +++ b/database/realm/RealmDatabaseManager.py @@ -70,7 +70,8 @@ def account_create(username, password, ip, salt, verifier): account = Account(name=username, password=password, ip=ip, gmlevel=int(config.Server.Settings.auto_create_gm_accounts), salt=salt, - verifier=verifier + verifier=verifier, + sessionkey="" ) realm_db_session.add(account) realm_db_session.flush() From 97a7f6cd19e8f4f8199a37ff4a30c685922ca7ff Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:04:38 -0600 Subject: [PATCH 19/46] Fix #1476 --- game/world/managers/objects/spell/aura/AuraManager.py | 3 ++- game/world/managers/objects/units/UnitManager.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/game/world/managers/objects/spell/aura/AuraManager.py b/game/world/managers/objects/spell/aura/AuraManager.py index febac3aa7..6553f746e 100644 --- a/game/world/managers/objects/spell/aura/AuraManager.py +++ b/game/world/managers/objects/spell/aura/AuraManager.py @@ -469,7 +469,8 @@ def _write_aura_flag_to_unit(self, aura, clear=False, is_refresh=False): self.current_flags &= ~(0x9 << byte) field_index = UnitFields.UNIT_FIELD_AURAFLAGS + (aura.index >> 3) - self.unit_mgr.set_uint32(field_index, self.current_flags, force=is_refresh or clear) + force_update = is_refresh or clear and self.unit_mgr.is_player() + self.unit_mgr.set_uint32(field_index, self.current_flags, force=force_update) def get_next_aura_index(self, aura) -> int: if aura.passive: diff --git a/game/world/managers/objects/units/UnitManager.py b/game/world/managers/objects/units/UnitManager.py index 6386ef4e5..e1d7469b2 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) + was_stunned = bool(self.unit_state & UnitStates.STUNNED) + is_stunned = bool(self.set_unit_state(UnitStates.STUNNED, active, index)) self.set_unit_flag(UnitFlags.UNIT_FLAG_DISABLE_ROTATE, active, index) if not was_stunned and is_stunned: + self.movement_manager.stop() self.spell_manager.remove_casts(remove_active=False) self.set_current_target(0) elif was_stunned and not is_stunned: From b9cdaa373b2e7d19f7cba0efd8f82dea9aa3ae45 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Tue, 4 Feb 2025 23:15:15 -0600 Subject: [PATCH 20/46] Fix #1478 --- game/world/managers/objects/spell/aura/AuraManager.py | 3 ++- game/world/managers/objects/units/UnitManager.py | 3 +++ game/world/managers/objects/units/player/PlayerManager.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/game/world/managers/objects/spell/aura/AuraManager.py b/game/world/managers/objects/spell/aura/AuraManager.py index 6553f746e..136c1530c 100644 --- a/game/world/managers/objects/spell/aura/AuraManager.py +++ b/game/world/managers/objects/spell/aura/AuraManager.py @@ -39,8 +39,9 @@ def add_aura(self, aura): # Application threat and negative aura application interrupts. if aura.harmful and self.unit_mgr != aura.caster and self.unit_mgr.can_attack_target(aura.caster): + threat = aura.get_effect_points() # Add threat for non-player targets against unit casters if the caster and target are not the same. - if aura.caster.is_unit(by_mask=True) and aura.source_spell.generates_threat(): + if aura.caster.is_unit(by_mask=True) and aura.source_spell.generates_threat() and threat: self.unit_mgr.threat_manager.add_threat(aura.caster, abs(aura.get_effect_points())) self.check_aura_interrupts(negative_aura_applied=True) diff --git a/game/world/managers/objects/units/UnitManager.py b/game/world/managers/objects/units/UnitManager.py index e1d7469b2..ff75a2fe3 100644 --- a/game/world/managers/objects/units/UnitManager.py +++ b/game/world/managers/objects/units/UnitManager.py @@ -1911,6 +1911,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: + 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/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): From a80b5c3cb7e90b61c9688ec3fa4f26ad309b5be7 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:42:29 -0600 Subject: [PATCH 21/46] Fix #1480 --- game/world/managers/objects/units/UnitManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/game/world/managers/objects/units/UnitManager.py b/game/world/managers/objects/units/UnitManager.py index ff75a2fe3..dfa6a9193 100644 --- a/game/world/managers/objects/units/UnitManager.py +++ b/game/world/managers/objects/units/UnitManager.py @@ -1220,6 +1220,7 @@ def set_stunned(self, active=True, index=-1) -> bool: self.set_unit_flag(UnitFlags.UNIT_FLAG_DISABLE_ROTATE, 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) From 2d699962e811167ad6f0fa0ec0683285007adfd6 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:05:31 -0600 Subject: [PATCH 22/46] Update UnitManager.py --- game/world/managers/objects/units/UnitManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/world/managers/objects/units/UnitManager.py b/game/world/managers/objects/units/UnitManager.py index dfa6a9193..b61464377 100644 --- a/game/world/managers/objects/units/UnitManager.py +++ b/game/world/managers/objects/units/UnitManager.py @@ -1912,7 +1912,7 @@ def notify_move_in_line_of_sight(self): surrounding_units = surrounding_units.values() for unit in surrounding_units: - if unit.unit_state & UnitStates.STUNNED: + if unit.unit_state & UnitStates.STUNNED or unit.unit_flags & UnitFlags.UNIT_FLAG_PACIFIED: continue unit_is_player = unit.is_player() From bbb3253e74ae84c2c2d37381f97822975e36590f Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:38:09 -0600 Subject: [PATCH 23/46] Fix #1479 --- etc/databases/world/updates/updates.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/etc/databases/world/updates/updates.sql b/etc/databases/world/updates/updates.sql index 2a1df2d85..adc6dac00 100644 --- a/etc/databases/world/updates/updates.sql +++ b/etc/databases/world/updates/updates.sql @@ -1171,5 +1171,18 @@ 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'); + + insert into applied_updates values ('020520251'); + end if; end $ delimiter ; \ No newline at end of file From 30ed301ec14e072b8185ff3e4c9e65c944321b6c Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:41:53 -0600 Subject: [PATCH 24/46] Fix #1481 --- etc/databases/world/updates/updates.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etc/databases/world/updates/updates.sql b/etc/databases/world/updates/updates.sql index adc6dac00..373ad5ee7 100644 --- a/etc/databases/world/updates/updates.sql +++ b/etc/databases/world/updates/updates.sql @@ -1181,6 +1181,8 @@ begin not atomic 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'); insert into applied_updates values ('020520251'); end if; From 60a94114c25b85bfd12acffd110091d0c6427700 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:43:39 -0600 Subject: [PATCH 25/46] Fix #1482 --- etc/databases/world/updates/updates.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etc/databases/world/updates/updates.sql b/etc/databases/world/updates/updates.sql index 373ad5ee7..d19bfcf35 100644 --- a/etc/databases/world/updates/updates.sql +++ b/etc/databases/world/updates/updates.sql @@ -1183,6 +1183,8 @@ begin not atomic (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'); insert into applied_updates values ('020520251'); end if; From bfa0671f55a644ec6bfd586682fa3207b138713a Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:03:18 -0600 Subject: [PATCH 26/46] Fix #1466 --- etc/databases/world/updates/updates.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etc/databases/world/updates/updates.sql b/etc/databases/world/updates/updates.sql index d19bfcf35..ecaff325b 100644 --- a/etc/databases/world/updates/updates.sql +++ b/etc/databases/world/updates/updates.sql @@ -1186,6 +1186,8 @@ begin not atomic 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 $ From 3539f235827cf397abc1047323c0effc192a11b9 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:51:46 -0600 Subject: [PATCH 27/46] Spell miss threat. --- game/world/managers/objects/spell/CastingSpell.py | 3 +++ game/world/managers/objects/spell/SpellManager.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/game/world/managers/objects/spell/CastingSpell.py b/game/world/managers/objects/spell/CastingSpell.py index cfcdd6c29..7513e9026 100644 --- a/game/world/managers/objects/spell/CastingSpell.py +++ b/game/world/managers/objects/spell/CastingSpell.py @@ -293,6 +293,9 @@ def is_far_sight(self): def generates_threat(self): return not self.spell_entry.AttributesEx & SpellAttributesEx.SPELL_ATTR_EX_NO_THREAT + 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. if self.spell_target_mask != SpellTargetMask.SELF: 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) From 4a2f51806daffa23df9ef303bfee2bfccc863717 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:51:44 -0600 Subject: [PATCH 28/46] Handle Sleep/Sap threat ignore through ExtendedSpellData --- game/world/managers/objects/spell/CastingSpell.py | 5 +++-- game/world/managers/objects/spell/ExtendedSpellData.py | 7 +++++++ game/world/managers/objects/spell/aura/AuraManager.py | 5 ++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/game/world/managers/objects/spell/CastingSpell.py b/game/world/managers/objects/spell/CastingSpell.py index 7513e9026..be77191a1 100644 --- a/game/world/managers/objects/spell/CastingSpell.py +++ b/game/world/managers/objects/spell/CastingSpell.py @@ -11,7 +11,7 @@ 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, SpellEffectMechanics 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 @@ -291,7 +291,8 @@ 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 SpellEffectMechanics.aura_effect_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 diff --git a/game/world/managers/objects/spell/ExtendedSpellData.py b/game/world/managers/objects/spell/ExtendedSpellData.py index 1ae63b7b1..cc0fa1f0a 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,8 @@ 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 + + # According to evidence from screenshots, neither Sleep nor Sap gets the player in combat. + @staticmethod + def aura_effect_should_generate_threat(spell_id) -> bool: + return spell_id not in SpellEffectMechanics._SLEEP_MECHANIC_SPELLS + SpellEffectMechanics._SAP_SPELLS diff --git a/game/world/managers/objects/spell/aura/AuraManager.py b/game/world/managers/objects/spell/aura/AuraManager.py index 136c1530c..6d4635a68 100644 --- a/game/world/managers/objects/spell/aura/AuraManager.py +++ b/game/world/managers/objects/spell/aura/AuraManager.py @@ -39,10 +39,9 @@ def add_aura(self, aura): # Application threat and negative aura application interrupts. if aura.harmful and self.unit_mgr != aura.caster and self.unit_mgr.can_attack_target(aura.caster): - threat = aura.get_effect_points() # Add threat for non-player targets against unit casters if the caster and target are not the same. - if aura.caster.is_unit(by_mask=True) and aura.source_spell.generates_threat() and threat: - self.unit_mgr.threat_manager.add_threat(aura.caster, abs(aura.get_effect_points())) + if aura.caster.is_unit(by_mask=True) and aura.source_spell.generates_threat(): + self.unit_mgr.threat_manager.add_threat(aura.caster) self.check_aura_interrupts(negative_aura_applied=True) From cf7afaae14db245c3407bed429556366e1aa1c1b Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:56:44 -0600 Subject: [PATCH 29/46] Update AuraManager.py --- game/world/managers/objects/spell/aura/AuraManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/world/managers/objects/spell/aura/AuraManager.py b/game/world/managers/objects/spell/aura/AuraManager.py index 6d4635a68..6553f746e 100644 --- a/game/world/managers/objects/spell/aura/AuraManager.py +++ b/game/world/managers/objects/spell/aura/AuraManager.py @@ -41,7 +41,7 @@ def add_aura(self, aura): if aura.harmful and self.unit_mgr != aura.caster and self.unit_mgr.can_attack_target(aura.caster): # Add threat for non-player targets against unit casters if the caster and target are not the same. if aura.caster.is_unit(by_mask=True) and aura.source_spell.generates_threat(): - self.unit_mgr.threat_manager.add_threat(aura.caster) + self.unit_mgr.threat_manager.add_threat(aura.caster, abs(aura.get_effect_points())) self.check_aura_interrupts(negative_aura_applied=True) From b6fb35c9ee377096c1000eb0488228de7b12a103 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:40:06 -0600 Subject: [PATCH 30/46] er --- game/world/managers/objects/spell/CastingSpell.py | 6 +++--- game/world/managers/objects/spell/ExtendedSpellData.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/game/world/managers/objects/spell/CastingSpell.py b/game/world/managers/objects/spell/CastingSpell.py index be77191a1..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, SpellEffectMechanics +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, \ @@ -292,7 +292,7 @@ def is_far_sight(self): def generates_threat(self): return (not self.spell_entry.AttributesEx & SpellAttributesEx.SPELL_ATTR_EX_NO_THREAT - and SpellEffectMechanics.aura_effect_should_generate_threat(self.spell_entry.ID)) + 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 diff --git a/game/world/managers/objects/spell/ExtendedSpellData.py b/game/world/managers/objects/spell/ExtendedSpellData.py index cc0fa1f0a..715bc75ec 100644 --- a/game/world/managers/objects/spell/ExtendedSpellData.py +++ b/game/world/managers/objects/spell/ExtendedSpellData.py @@ -325,7 +325,11 @@ 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: + _SLEEP_SPELLS = (700, 1090, 2937) + _SAP_SPELLS = (2070, 6770, 6771) + # According to evidence from screenshots, neither Sleep nor Sap gets the player in combat. @staticmethod - def aura_effect_should_generate_threat(spell_id) -> bool: - return spell_id not in SpellEffectMechanics._SLEEP_MECHANIC_SPELLS + SpellEffectMechanics._SAP_SPELLS + def spell_should_generate_threat(spell_id) -> bool: + return spell_id not in SpellThreatMechanics._SLEEP_SPELLS and spell_id not in SpellThreatMechanics._SAP_SPELLS \ No newline at end of file From 3bef0da7f7298ae97546a3029869fbe43598b0e9 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:22:06 -0600 Subject: [PATCH 31/46] Update ExtendedSpellData.py --- game/world/managers/objects/spell/ExtendedSpellData.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/game/world/managers/objects/spell/ExtendedSpellData.py b/game/world/managers/objects/spell/ExtendedSpellData.py index 715bc75ec..8b0e10fc5 100644 --- a/game/world/managers/objects/spell/ExtendedSpellData.py +++ b/game/world/managers/objects/spell/ExtendedSpellData.py @@ -326,10 +326,13 @@ def get_mechanic_for_aura_effect(aura_type, spell_id) -> Optional[SpellMechanic] spell_id in SpellEffectMechanics._SLEEP_MECHANIC_SPELLS else None class SpellThreatMechanics: - _SLEEP_SPELLS = (700, 1090, 2937) - _SAP_SPELLS = (2070, 6770, 6771) + _NON_COMBAT_STUN_SPELLS = ( + 700, 1090, 2937, # Sleep. + 2070, 6770, 6771, # Sap. + 6358 # Seduction. + ) # According to evidence from screenshots, neither Sleep nor Sap gets the player in combat. @staticmethod def spell_should_generate_threat(spell_id) -> bool: - return spell_id not in SpellThreatMechanics._SLEEP_SPELLS and spell_id not in SpellThreatMechanics._SAP_SPELLS \ No newline at end of file + return spell_id not in SpellThreatMechanics._NON_COMBAT_STUN_SPELLS \ No newline at end of file From 0adede979b90e23b4cf4bcfe7884ad9b6106a1da Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:22:56 -0600 Subject: [PATCH 32/46] Update ExtendedSpellData.py --- game/world/managers/objects/spell/ExtendedSpellData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/world/managers/objects/spell/ExtendedSpellData.py b/game/world/managers/objects/spell/ExtendedSpellData.py index 8b0e10fc5..48dbff140 100644 --- a/game/world/managers/objects/spell/ExtendedSpellData.py +++ b/game/world/managers/objects/spell/ExtendedSpellData.py @@ -332,7 +332,7 @@ class SpellThreatMechanics: 6358 # Seduction. ) - # According to evidence from screenshots, neither Sleep nor Sap gets the player in combat. + # 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 From d11f2f93ae2d565a31790660396bc735c8ae4f75 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:37:53 -0600 Subject: [PATCH 33/46] Prevent non players from forcing updates. --- game/world/managers/objects/ObjectManager.py | 5 +++++ game/world/managers/objects/spell/aura/AuraManager.py | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/game/world/managers/objects/ObjectManager.py b/game/world/managers/objects/ObjectManager.py index 75ef65618..299ee2c81 100644 --- a/game/world/managers/objects/ObjectManager.py +++ b/game/world/managers/objects/ObjectManager.py @@ -314,6 +314,7 @@ 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): + force = force and self.is_player() 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. @@ -325,6 +326,7 @@ def get_int32(self, index): return self._get_value_by_type_at('i', index) def set_uint32(self, index, value, force=False): + force = force and self.is_player() 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. @@ -336,6 +338,7 @@ def get_uint32(self, index): return self._get_value_by_type_at('I', index) def set_int64(self, index, value, force=False): + force = force and self.is_player() 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. @@ -347,6 +350,7 @@ def get_int64(self, index): return self._get_value_by_type_at('q', index) def set_uint64(self, index, value, force=False): + force = force and self.is_player() 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. @@ -358,6 +362,7 @@ def get_uint64(self, index): return self._get_value_by_type_at('Q', index) def set_float(self, index, value, force=False): + force = force and self.is_player() 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. diff --git a/game/world/managers/objects/spell/aura/AuraManager.py b/game/world/managers/objects/spell/aura/AuraManager.py index 6553f746e..febac3aa7 100644 --- a/game/world/managers/objects/spell/aura/AuraManager.py +++ b/game/world/managers/objects/spell/aura/AuraManager.py @@ -469,8 +469,7 @@ def _write_aura_flag_to_unit(self, aura, clear=False, is_refresh=False): self.current_flags &= ~(0x9 << byte) field_index = UnitFields.UNIT_FIELD_AURAFLAGS + (aura.index >> 3) - force_update = is_refresh or clear and self.unit_mgr.is_player() - self.unit_mgr.set_uint32(field_index, self.current_flags, force=force_update) + self.unit_mgr.set_uint32(field_index, self.current_flags, force=is_refresh or clear) def get_next_aura_index(self, aura) -> int: if aura.passive: From 21dbdd2bccaf134f7e5d29a4588e07fb6e7bef47 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:12:56 -0600 Subject: [PATCH 34/46] More changes regarding #1480 --- game/world/managers/objects/units/UnitManager.py | 3 ++- .../objects/units/movement/behaviors/ChaseMovement.py | 4 ++-- .../objects/units/movement/behaviors/GroupMovement.py | 5 ++++- .../managers/objects/units/movement/behaviors/PetMovement.py | 5 ++++- .../objects/units/movement/behaviors/WanderingMovement.py | 5 ++++- .../objects/units/movement/behaviors/WaypointMovement.py | 5 ++++- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/game/world/managers/objects/units/UnitManager.py b/game/world/managers/objects/units/UnitManager.py index b61464377..4c14c1f11 100644 --- a/game/world/managers/objects/units/UnitManager.py +++ b/game/world/managers/objects/units/UnitManager.py @@ -1217,7 +1217,6 @@ def set_stunned(self, active=True, index=-1) -> bool: was_stunned = bool(self.unit_state & UnitStates.STUNNED) is_stunned = bool(self.set_unit_state(UnitStates.STUNNED, active, index)) - self.set_unit_flag(UnitFlags.UNIT_FLAG_DISABLE_ROTATE, active, index) if not was_stunned and is_stunned: # Force move behavior stop. @@ -1229,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): 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 From d986aec867f9360b89b30a4c785b1bdcb2a60319 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 15 Feb 2025 22:19:02 -0600 Subject: [PATCH 35/46] Minor. --- game/realm/AccountManager.py | 11 +++++------ utils/Srp6.py | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/game/realm/AccountManager.py b/game/realm/AccountManager.py index dea1607ed..a9287a644 100644 --- a/game/realm/AccountManager.py +++ b/game/realm/AccountManager.py @@ -4,6 +4,7 @@ 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): @@ -52,17 +53,15 @@ def calculate_client_server_proof(self, client_public_key): 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', 12, 1) + data = pack('<2B', AuthCode.AUTH_OK, Srp6ResponseType.AuthProof) data += s_M2 data += pack(' bytes: - data = pack('<2B', 12, 0) - data += pack(' Date: Tue, 18 Feb 2025 21:56:53 -0600 Subject: [PATCH 36/46] Extract build_socket func. --- game/login/LoginManager.py | 15 ++------------- game/realm/RealmManager.py | 17 ++++------------- game/update/UpdateManager.py | 15 ++------------- network/sockets/SocketBuilder.py | 15 +++++++++++++++ network/sockets/__init__.py | 0 5 files changed, 23 insertions(+), 39 deletions(-) create mode 100644 network/sockets/SocketBuilder.py create mode 100644 network/sockets/__init__.py diff --git a/game/login/LoginManager.py b/game/login/LoginManager.py index b0384d655..3afcaf7ac 100644 --- a/game/login/LoginManager.py +++ b/game/login/LoginManager.py @@ -3,28 +3,17 @@ 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 build_socket(address, port): - 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(2) - return socket_ - @staticmethod def start_login(running, login_server_ready): login_host = config.Server.Connection.Login.host login_port = config.Server.Connection.Login.port - with LoginManager.build_socket(login_host, login_port) as server_socket: + 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]}') diff --git a/game/realm/RealmManager.py b/game/realm/RealmManager.py index 9b112e4b9..6f74fcb4f 100644 --- a/game/realm/RealmManager.py +++ b/game/realm/RealmManager.py @@ -4,6 +4,7 @@ from game.world.WorldSessionStateHandler import RealmDatabaseManager from network.packet.PacketWriter import * +from network.sockets.SocketBuilder import SocketBuilder from utils.ConfigManager import config from utils.Logger import Logger from utils.constants import EnvVars @@ -13,17 +14,7 @@ class RealmManager: - @staticmethod - def build_socket(address, port): - 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(2) - return socket_ + @staticmethod def serve_realmlist(sck): @@ -72,7 +63,7 @@ def redirect_to_world(sck): @staticmethod def start_realm(running, realm_server_ready): local_realm = REALMLIST[config.Server.Connection.Realm.local_realm_id] - with RealmManager.build_socket(local_realm.realm_address, local_realm.realm_port) as server_socket: + with SocketBuilder.build_socket(local_realm.realm_address, local_realm.realm_port, timeout=2) as server_socket: server_socket.listen() real_binding = server_socket.getsockname() # Make sure all characters have online = 0 on realm start. @@ -99,7 +90,7 @@ def start_realm(running, realm_server_ready): @staticmethod def start_proxy(running, proxy_server_ready): local_realm = REALMLIST[config.Server.Connection.Realm.local_realm_id] - with RealmManager.build_socket(local_realm.proxy_address, local_realm.proxy_port) as server_socket: + with SocketBuilder.build_socket(local_realm.proxy_address, local_realm.proxy_port, timeout=2) 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]}') diff --git a/game/update/UpdateManager.py b/game/update/UpdateManager.py index 6799660fc..823c89ab9 100644 --- a/game/update/UpdateManager.py +++ b/game/update/UpdateManager.py @@ -3,28 +3,17 @@ import traceback from game.update.UpdateSessionStateHandler import UpdateSessionStateHandler +from network.sockets.SocketBuilder import SocketBuilder from utils.ConfigManager import config from utils.Logger import Logger class UpdateManager: - @staticmethod - def build_socket(address, port): - 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(2) - return socket_ - @staticmethod def start_update(running, update_server_ready): update_host = config.Server.Connection.Update.host update_port = config.Server.Connection.Update.port - with UpdateManager.build_socket(update_host, update_port) as server_socket: + with SocketBuilder.build_socket(update_host, update_port, timeout=2) as server_socket: server_socket.listen() real_binding = server_socket.getsockname() Logger.success(f'Update server started, listening on {real_binding[0]}:{real_binding[1]}') 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/network/sockets/__init__.py b/network/sockets/__init__.py new file mode 100644 index 000000000..e69de29bb From 87adca70a1e40b9556e6d0d43db956b5ed8b3cb5 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:08:21 -0600 Subject: [PATCH 37/46] Update UpdateSessionStateHandler.py --- game/update/UpdateSessionStateHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/update/UpdateSessionStateHandler.py b/game/update/UpdateSessionStateHandler.py index 6262cdcd2..9bf0dbea9 100644 --- a/game/update/UpdateSessionStateHandler.py +++ b/game/update/UpdateSessionStateHandler.py @@ -7,7 +7,7 @@ def __init__(self, client_socket, client_address): self.client_socket = client_socket self.client_address = client_address - # TODO: UpdateServer seems to use some kind of RSYNC protocol for files. + # TODO: UpdateServer seems to use some kind of RSYNC protocol for file patching. def handle(self): self.disconnect() From 7abe6dd54c6f1ef51d379e197b1ee3b243f21fcd Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:23:16 -0600 Subject: [PATCH 38/46] Update ObjectManager.py --- game/world/managers/objects/ObjectManager.py | 65 +++++++------------- 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/game/world/managers/objects/ObjectManager.py b/game/world/managers/objects/ObjectManager.py index 299ee2c81..e895d94d8 100644 --- a/game/world/managers/objects/ObjectManager.py +++ b/game/world/managers/objects/ObjectManager.py @@ -314,62 +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): - force = force and self.is_player() - 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): - force = force and self.is_player() - 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): - force = force and self.is_player() - 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): - force = force and self.is_player() - 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): - force = force and self.is_player() - 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) @@ -388,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, signature, force=False): + force = force and self.is_player() + if force or self.update_packet_factory.should_update(index, value, signature): + self.update_packet_factory.update(index, value, signature) + 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 From 0327e902cd6c9f76723ad88c0bde967faf2bfebf Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:25:01 -0600 Subject: [PATCH 39/46] Update ObjectManager.py --- game/world/managers/objects/ObjectManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/game/world/managers/objects/ObjectManager.py b/game/world/managers/objects/ObjectManager.py index e895d94d8..b6e1da242 100644 --- a/game/world/managers/objects/ObjectManager.py +++ b/game/world/managers/objects/ObjectManager.py @@ -358,10 +358,10 @@ def _get_value_by_type_at(self, value_type, index): return unpack(f'<{value_type}', value)[0] - def _set_value(self, index, value, signature, force=False): + 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, signature): - self.update_packet_factory.update(index, value, signature) + 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 From 431b4adc00455a46912a183f33bd97b0d321edb5 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:51:58 -0600 Subject: [PATCH 40/46] Srp6 - Use explicit variable names. - Fix username parsing for CMSG_AUTH_SRP6_BEGIN --- game/realm/AccountManager.py | 8 +- .../handlers/interface/AuthSessionHandler.py | 8 +- network/packet/PacketReader.py | 6 +- utils/Srp6.py | 472 ++---------------- 4 files changed, 66 insertions(+), 428 deletions(-) diff --git a/game/realm/AccountManager.py b/game/realm/AccountManager.py index a9287a644..6d3847273 100644 --- a/game/realm/AccountManager.py +++ b/game/realm/AccountManager.py @@ -44,17 +44,17 @@ def get_verifier_bytes(self) -> bytes: 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_S = Srp6.calculate_server_S_key(client_public_key, self.get_verifier_bytes(), u, self._server_private_key) - self._session_key = Srp6.calculate_interleaved(s_S) + 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) + 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 += s_m2 data += pack('= fixed_length): break char_list.append(cc) + chars += 1 return ''.join(char_list) diff --git a/utils/Srp6.py b/utils/Srp6.py index 550a3ce40..ff20327a7 100644 --- a/utils/Srp6.py +++ b/utils/Srp6.py @@ -1,26 +1,8 @@ import os, hashlib -from ctypes import c_ubyte ''' -A Client public key -a Client private key -B Server public key -b Server private key N Large safe prime g Generator -k K value -s Salt -U Username -p Password -v Password verifier -M1 Client proof (proof first sent by client, calculated by both) -M2 Server proof (proof first sent by server, calculated by both) -M (Client or server) proof -S S key -K Session key - -LS Login Server -WS World Server ''' zero = bytes([0, 0, 0, 0]) @@ -38,12 +20,12 @@ class Srp6: ]) @staticmethod - def calculate_x(U:str, p:str, s:bytes) -> bytes: + def calculate_x(username:str, password:str, salt:bytes) -> bytes: """ - x = SHA1( s | SHA1( U | : | p )) + x = SHA1( salt | SHA1( username | : | password )) """ - interim = SHA1((U.upper() + ':' + p.upper()).encode()).digest() - x = SHA1(s + interim).digest() + interim = SHA1((username.upper() + ':' + password.upper()).encode()).digest() + x = SHA1(salt + interim).digest() return x @staticmethod @@ -51,55 +33,55 @@ def generate_salt(): return os.urandom(32) @staticmethod - def calculate_password_verifier(U:str, p:str, s:bytes) -> bytes: + def calculate_password_verifier(username:str, password:str, salt:bytes) -> bytes: """ - v = g^x % N + password_verifier = g^x % N """ - x = int.from_bytes(Srp6.calculate_x(U, p, s), byteorder='little') + 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(v:bytes, b:bytes) -> bytes: + def calculate_server_public_key(password_verifier:bytes, server_private_key:bytes) -> bytes: """ - B = (k * v + (g^b % N)) % N + server_public_key = (k * password_verifier + (g^server_private_key % N)) % N """ - v = int.from_bytes(v, byteorder='little') - b = int.from_bytes(b, byteorder='little') - B = (Srp6.k * v + pow(Srp6.g, b, Srp6.N)) % Srp6.N - assert B % Srp6.N != 0 - return int.to_bytes(B, 32, 'little') + 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(a:bytes, B:bytes, x:bytes, u:bytes) ->bytes: + def calculate_client_s_key(client_private_key:bytes, server_public_key:bytes, x:bytes, u:bytes) ->bytes: """ - S = (B - (k * (g^x % N)))^(a + u * x) % N + s_key = (server_public_key - (k * (g^x % N)))^(client_private_key + u * x) % N """ - a = int.from_bytes(a, byteorder='little') - B = int.from_bytes(B, byteorder='little') + 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 = pow((B - Srp6.k * pow(Srp6.g, x, Srp6.N)), (a + u * x), Srp6.N) - return int.to_bytes(S, 32, '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(A, v, u, b) -> bytes: + def calculate_server_s_key(client_public_key, password_verifier, u, server_private_key) -> bytes: """ - S = (A * (v^u % N))^b % N, + s_key = (client_public_key * (password_verifier^u % N))^server_private_key % N, """ - A = int.from_bytes(A, byteorder='little') - v = int.from_bytes(v, byteorder='little') + 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') - b = int.from_bytes(b, byteorder='little') - S = pow((A * pow(v, u, Srp6.N)), b, Srp6.N) - return int.to_bytes(S, 32, '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(A:bytes, B:bytes) -> bytes: + def calculate_u(client_public_key:bytes, server_public_key:bytes) -> bytes: """ - u = SHA1( A | B ) + u = SHA1( client_public_key | server_public_key ) """ - u = SHA1(A + B).digest() + u = SHA1(client_public_key+ server_public_key).digest() return u @staticmethod @@ -109,390 +91,44 @@ def calculate_interleaved(s_key:bytes) -> bytes: """ 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() - K = bytes(x for y in zip(G, H) for x in y) - return K + 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(A:bytes, M1:bytes, K:bytes) -> bytes: + def calculate_server_proof(client_public_key:bytes, m1:bytes, session_key:bytes) -> bytes: """ - M2 = SHA1(A | M1 | K) + m2 = SHA1(client_public_key | m1 | session_key) """ - M2 = SHA1(A + M1 + K).digest() - return M2 + m2 = SHA1(client_public_key + m1 + session_key).digest() + return m2 @staticmethod - def calculate_xor_hash() -> bytes: + def calculate_client_proof(x:bytes, username:str, session_key:bytes, client_public_key:bytes, + server_public_key:bytes, salt:bytes) -> bytes: """ - SHA1(N) XOR SHA1(g) + m1 = SHA1( x | SHA1(username) | salt | client_public_key | server_public_key | session_key ) """ - x1 = int.to_bytes(Srp6.g, 1, 'little') - x2 = bytes.fromhex('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7')[::-1] - t1 = SHA1(x1).digest() - t2 = SHA1(x2).digest() - assert len(t1) == len(t2) == 20 - result = (c_ubyte * 20)() - for n in range(20): - result[n] = t1[n] ^ t2[n] - return bytes(result) + 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_proof(X:bytes, U:str, K:bytes, A:bytes, B:bytes, s:bytes) -> bytes: + def calculate_client_public_key(a:bytes) -> bytes: """ - M1 = SHA1( X | SHA1(U) | s | A | B | K ) + client_public_key = generator^private_client_key % N """ - U = SHA1(U.upper().encode()).digest() - M1 = SHA1(X + U + s + A + B + K).digest() - return M1 - - @staticmethod - def calculate_client_public_key(a:bytes) -> bytes: - ''' - A = g^a % N - ''' - a = int.from_bytes(a, byteorder='little') - A = pow(Srp6.g, a, Srp6.N) - assert A % Srp6.N != 0 - return int.to_bytes(A, 32, 'little') - - @staticmethod - def calculate_reconnect_proof(username:str, client_data:bytes, server_data:bytes, session_key:bytes) -> bytes: - ''' - SHA1( username | client_data | server_data | session_key ) - ''' - return SHA1(username.upper().encode() + client_data + server_data + session_key).digest() - - @staticmethod - def encrypt(data:bytes, session_key:bytes) -> bytes: - ''' - E = (x ^ S) + L - ''' - index = 0 - last_value = 0 - size = len(data) - result = (c_ubyte * size)() - session_key_length = len(session_key) - for n in range(size): - unencrypted = data[n] - encrypted = (unencrypted ^ session_key[index]) + last_value - index = (index + 1) % session_key_length - last_value = encrypted - result[n] = encrypted - return bytes(result) - - @staticmethod - def decrypt(data:bytes, session_key:bytes) -> bytes: - ''' - x = (E - L) ^ S - ''' - index = 0 - last_value = 0 - size = len(data) - result = (c_ubyte * size)() - session_key_length = len(session_key) - for n in range(size): - encrypted = data[n] - unencrypted = (encrypted - last_value) ^ session_key[index] - index = (index + 1) % session_key_length - last_value = encrypted - result[n] = unencrypted - return bytes(result) + 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() - - - - # # bytes.fromhex()[::-1] == big -> little - # - # # test1 - # h1 = SHA1('test'.encode()).hexdigest() - # assert h1 == 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3' - # h2 = SHA1(b'\x53\x51').hexdigest() - # assert h2 == '0c3d7a19ac7c627290bf031ec3df76277b0f7f75' - # - # - # # test2 - # salt = bytes.fromhex('AFE5D28E925DBB3DAFED5D91ACA0928940E8FBFEF2D2A3CC154ADA0FE6ABEF6F')[::-1] - # expected = bytes.fromhex('21B4153B0A938D0A69D28F2690CC3F79A99A13C40CACB525B3B79D4201EB33FF')[::-1] - # #------------------------- - # U = 'LF2BGFQIFQ3HZ1ZF' - # p = 'MVRVMUJFWRA0IBVK' - # s = salt - # v = calculate_password_verifier(U, p, s) - # assert v == expected - # - # - # # test3 - # salt = bytes.fromhex('CAC94AF32D817BA64B13F18FDEDEF92AD4ED7EF7AB0E19E9F2AE13C828AEAF57')[::-1] - # expected = bytes.fromhex('D927E98BE3E9AF84FDC99DE9034F8E70ED7E90D6')[::-1] - # #------------------------- - # U = 'USERNAME123' - # p = 'PASSWORD123' - # s = salt - # x = calculate_x(U, p, s) - # assert expected == x - # - # - # # test4 - # expected = bytes.fromhex('E2F9A0F1E824006C98DA753448E743F7DAA1EAA1')[::-1] - # #------------------------- - # U = '00XD0QOSA9L8KMXC' - # p = '43R4Z35TKBKFW8JI' - # s = salt - # x = calculate_x(U, p, s) - # assert expected == x - # - # - # # test5 - # password_verifier = bytes.fromhex('870A98A3DA8CCAFE6B2F4B0C43A022A0C6CEF4374BA4A50CEBF3FACA60237DC4')[::-1] - # server_private_key = bytes.fromhex('ACDCB7CB1DE67DB1D5E0A37DAE80068BCCE062AE0EDA0CBEADF560BCDAE6D6B9')[::-1] - # expected = bytes.fromhex('85A204C987B68764FA69C523E32B940D1E1822B9E0F134FDC5086B1408A2BB43')[::-1] - # #------------------------- - # v = password_verifier - # b = server_private_key - # B = calculate_server_public_key(v, b) - # assert expected == B - # - # - # # test6 - # server_public_key = bytes.fromhex('E232D2C71AD1BF58DB9F7DBE51FFE271B6BDC61524F2E6B32ABFFFCAB09D09AB')[::-1] - # client_private_key = bytes.fromhex('FC3D610C4E2CEC5ECC7E47344D0ED81D2ACB938AB198EC7E2ED474AEFCC3ABD1')[::-1] - # x = bytes.fromhex('A4A7CB7DFBE00D26EE06F6B3DACC51E5779D7E8B')[::-1] - # u = bytes.fromhex('FDAFAEF0E77F0FE1BD2956CF1820D4BC964E5283')[::-1] - # expected = bytes.fromhex('3898DF5193EA6AA8111524A253DB480A51EA6160D1E41BC4B662420299B4A435')[::-1] - # #------------------------- - # a = client_private_key - # B = server_public_key - # S = calculate_client_S_key(a, B, x, u) - # assert expected == S - # - # - # # test7 - # client_public_key = bytes.fromhex('51CCDDFACF7F960EDF5030F09F0B033C0D08DB1E43FCBA3A92ABB4BE3535D1DB')[::-1] - # password_verifier = bytes.fromhex('6FC7D4ACFCFFFDCF780EE9BBD17AE507FFCDF586F83B2C9AEE2198F195DB3AB5')[::-1] - # u = bytes.fromhex('F9CEDDD82E776BEDB1A94852A9A7FFA4FCADD5DE')[::-1] - # server_private_key = bytes.fromhex('A5DBBFCB4C7A1B7C3041CAC9DDBD36CD646F9FBABDAD66A019BCBB8FEDF2FAAE')[::-1] - # expected = bytes.fromhex('3503B289A60D6DD59EBD6FD88DF24836833433E39048ECAFF7E887313554F85C')[::-1] - # #------------------------- - # A = client_public_key - # v = password_verifier - # b = server_private_key - # S = calculate_server_S_key(A, v, u, b) - # assert expected == S - # - # - # # test8 - # client_public_key = bytes.fromhex('6FCEEEE7D40AAF0C7A08DFE1EFD3FCE80A152AA436CECB77FC06DAF9E9E5BDF3')[::-1] - # server_public_key = bytes.fromhex('F8CD769BDE603FC8F48B9BE7C5BEAAA7BD597ABDBDAC1AEFCACF0EE13443A3B9')[::-1] - # expected = bytes.fromhex('1309BD7851A1A505B95D6F60A8D884133458D24E')[::-1] - # #------------------------- - # A = client_public_key - # B = server_public_key - # u = calculate_u(A, B) - # assert expected == u - # - # - # # test9 - # s_key = bytes.fromhex('8F4CEBD60DFC34E5C007E51BD4F3A4FF2BC1D930E2D3EA770D8D3EEDFF2DCCFC') - # expected = bytes.fromhex('EE144E1AE08DAC891AB63ABC42BF89738003343422E6B58131BEE4C3087A7027E55A7216D18D556C') - # #------------------------- - # session_key = calculate_interleaved(s_key) - # assert expected == session_key - # - # - # # test10 - # client_public_key = bytes.fromhex('BFD1AC65C8DAAAD88BF9DFF9AF8D1DCDF11DFD0C7E398EDCDF5DBBD08EFB39D3')[::-1] - # client_proof = bytes.fromhex('7EBBC190D9AB2DC0CD891372CB30DF1ED35CDA1E')[::-1] - # session_key = bytes.fromhex('9382b5e82c16e1105b8e8e88a99118811d88170fad6e8b35f236dbebbcc9c99bcab6cc9f8fe67648') - # expected = bytes.fromhex('269E3A3EF5DCD15944F043513BDA20D20FEBA2E0')[::-1] - # #------------------------- - # A = client_public_key - # M1 = client_proof - # K = session_key - # M2 = calculate_server_proof(A, M1, K) - # assert expected == M2 - # - # - # # test11 - # #------------------------- - # X = calculate_xor_hash() - # assert xorNg == X - # - # - # # test12 - # username = '7WG6SHZL33JMGPO4' - # session_key = bytes.fromhex('77a4d39cf9c0bf373ef870bd2941c339c575fdd1cbaa31c919ea7bd5023267d303e20fec9a9c402f') - # client_public_key = bytes.fromhex('0095FE039AFE5E1BADE9AC0CAEC3CB73D2D08BBF4CA8ADDBCDF0CE709ED5103F')[::-1] - # server_public_key = bytes.fromhex('00B0C41F58CCE894CFB816FA72CA344C9FE2ED7CE799452ADBA7ABDCD26EAE75')[::-1] - # salt = bytes.fromhex('00a4a09e0b5aca438b8cd837d0816ca26043dbd1eaef138eef72dcf3f696d03d')[::-1] - # expected = bytes.fromhex('7D07022B4064CCE633D679F61C6B212B6F8BC5C3')[::-1] - # #------------------------- - # U = username - # K = session_key - # A = client_public_key - # B = server_public_key - # s = salt - # M1 = calculate_client_proof(xorNg, U, K, A, B, s) - # assert expected == M1 - # - # - # # test13 - # client_private_key = bytes.fromhex('A47DD4CD70DA1B0EF7E1FA8C02DE68AF0CEFCC77ACA287FBC3ADCDE0E7B78FE7')[::-1] - # expected = bytes.fromhex('7186DF27C1A309B5B26E293CD00ADD01E7037E09116089F26E810FD2D962BC42')[::-1] - # #------------------------- - # a = client_private_key - # A = calculate_client_public_key(a) - # assert expected == A - # - # - # # test14 - # username = 'GXJ8M6VDUAC0JL9W' - # client_data = bytes.fromhex('DD801B2FBCF4F7ABC6023EFAAF6A9AEA') - # server_data = bytes.fromhex('0D27763BDEEF92CB273B7BC4EE72D0EC') - # session_key = bytes.fromhex('6A0E7B35C70C70DA142D57BF49FD25D84CCEE3D21CC1A10AD71323FB34F45F3006D606F1F39A6BB9') - # expected = bytes.fromhex('D94CE2B08B7FAC0919D7D5419D78CABFA372B6A9') - # #------------------------- - # reconnect = calculate_reconnect_proof(username, client_data, server_data, session_key) - # assert expected == reconnect - # - # - # # test15 - # session_key = bytes.fromhex('2EFEE7B0C177EBBDFF6676C56EFC2339BE9CAD14BF8B54BB5A86FBF81F6D424AA23CC9A3149FB175') - # data = bytes.fromhex('3d9ae196ef4f5be4df9ea8b9f4dd95fe68fe58b653cf1c2dbeaa0be167db9b27df32fd230f2eab9bd7e9b2f3fbf335d381ca') - # expected = bytes.fromhex('13777da3d109b912322a08841e3ff5bc92f4e98b77bb03997da999b22ae0b926a3b1e56580314b3932499ee11b9f7deb6915') - # #------------------------- - # encrypted_data = encrypt(data, session_key) - # assert expected == encrypted_data - # - # - # # test16 - # session_key = bytes.fromhex('2EFEE7B0C177EBBDFF6676C56EFC2339BE9CAD14BF8B54BB5A86FBF81F6D424AA23CC9A3149FB175') - # data = bytes.fromhex('3d9ae196ef4f5be4df9ea8b9f4dd95fe68fe58b653cf1c2dbeaa0be167db9b27df32fd230f2eab9bd7e9b2f3fbf335d381ca') - # expected = bytes.fromhex('13a3a0059817e73404d97cd455159b50d40af74a22f719aacb6a9a2e991982c61a6f0285f880cc8512ec2ef1c98fa923512f') - # #------------------------- - # unencrypted_data = decrypt(data, session_key) - # assert expected == unencrypted_data - # - # - # # test17 - # username = 'HQO7EWULX09Z4RE4' - # session_key = bytes.fromhex('77295B4E6745E8833293E07650252D635D5E4B14D2A9DA4FB1AE22FB00131E42C2B2EE7BF0D4D185')[::-1] - # server_seed = bytes.fromhex('2d0a01e2') - # client_seed = bytes.fromhex('a2ba5fb2') - # expected = bytes.fromhex('b26af9256f4bd20f0f11e2c786710542b92115bb') - # #------------------------- - # world_server_proof = calculate_world_server_proof(username, client_seed, server_seed, session_key) - # assert expected == world_server_proof - # - # - # - # # final test - # #------------------------- - # username = 'Mario' - # password = '5#BB-:*!skTu' - # - # # 1. create account, save s and v to database - # # salt - # s = os.urandom(32) - # # password verified - # v = calculate_password_verifier(username, password, s) - # - # - # # 2. [LogonChallenge] client -> LS: username - # - # - # # 3. [LogonChallenge] LS -> client: B, s, N, g - # # read s and v from database by username - # # server private key - # b = os.urandom(32) - # # server public key - # B = calculate_server_public_key(v, b) - # - # - # # 4. [LogonProof] client -> LS: A, M1 - # # client private key - # a = os.urandom(32) - # # client public key - # A = calculate_client_public_key(a) - # # client S key - # x = calculate_x(username, password, s) - # u = calculate_u(A, B) - # c_S = calculate_client_S_key(a, B, x, u) - # # client session key - # c_K = calculate_interleaved(c_S) - # # client proof - # c_M1 = calculate_client_proof(xorNg, username, c_K, A, B, s) - # - # - # # 5. [LogonProof] LS -> client: M2 - # # server S key - # u = calculate_u(A, B) - # s_S = calculate_server_S_key(A, v, u, b) - # # server session key - # s_K = calculate_interleaved(s_S) - # # check M - # s_M1 = calculate_client_proof(xorNg, username, s_K, A, B, s) - # # authenticated - # assert c_M1 == s_M1 - # # server proof - # s_M2 = calculate_server_proof(A, s_M1, s_K) - # - # - # # 6. [RealmList] ... - # c_M2 = calculate_server_proof(A, c_M1, c_K) - # # authenticated - # assert c_M2 == s_M2 - # - # - # # 7. [ReconnectChallenge] client -> LS: username - # - # - # # 8. [ReconnectChallenge] LS -> client: server_data - # # checks for an existing session with the username - # server_data = os.urandom(16) - # - # - # # 9. [ReconnectProof] client -> LS: client_data, client_proof - # client_data = os.urandom(16) - # client_proof = calculate_reconnect_proof(username, client_data, server_data, c_K) - # - # - # # 10. [ReconnectProof] ... - # server_proof = calculate_reconnect_proof(username, client_data, server_data, s_K) - # # authenticated - # assert server_proof == client_proof - # - # - # # 11. client connects to the WS - # - # - # # 12. [AuthChallenge] WS -> client: server_seed - # server_seed = os.urandom(4) - # - # - # # 13. [AuthSession] client -> server: client_seed, client_auth - # client_seed = os.urandom(4) - # client_auth = calculate_world_server_proof(username, client_seed, server_seed, c_K) - # - # - # # 14. [AuthSession] server -> client: encrypt_msg - # server_auth = calculate_world_server_proof(username, client_seed, server_seed, s_K) - # # authenticated - # assert client_auth == server_auth - # # send data - # server_msg = 'WeLcOmE oNbOaRd'.encode() - # encrypt_msg = encrypt(server_msg, s_K) - # - # - # # 15. [SessionMsg] client - # client_msg = decrypt(encrypt_msg, c_K) - # # verify - # assert server_msg == client_msg \ No newline at end of file From 95e108e62524923e062b23ef3286fef0e9a43201 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:42:26 -0600 Subject: [PATCH 41/46] Revert change to read_string(). --- .../opcode_handling/handlers/interface/AuthSessionHandler.py | 3 +-- network/packet/PacketReader.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py index a973c5ea3..f96135d22 100644 --- a/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py +++ b/game/world/opcode_handling/handlers/interface/AuthSessionHandler.py @@ -15,7 +15,7 @@ def handle_srp6_begin(auth_session, reader): '= fixed_length): + if cc == terminator: break char_list.append(cc) chars += 1 From f67fbd49a24e47fc33cc74fae480f104d8593202 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:02:57 -0600 Subject: [PATCH 42/46] Update Srp6.py --- utils/Srp6.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/Srp6.py b/utils/Srp6.py index ff20327a7..bf7ff1c37 100644 --- a/utils/Srp6.py +++ b/utils/Srp6.py @@ -3,6 +3,7 @@ ''' N Large safe prime g Generator +k K value ''' zero = bytes([0, 0, 0, 0]) From d39bc0c6ba5b93e38d878698ebdaa57e53665a8d Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 22 Mar 2025 18:39:43 -0600 Subject: [PATCH 43/46] Allow building nav files using namigator mapbuild binding. --- game/world/managers/maps/MapManager.py | 11 ++ game/world/managers/maps/helpers/Mapbuild.py | 24 ++++ main.py | 8 +- requirements.txt | 1 + tools/extractors/Extractor.py | 34 ++++++ .../MapExtractor.py | 59 ++++------ tools/extractors/NavExtractor.py | 107 ++++++++++++++++++ .../{map_extractor => extractors}/__init__.py | 0 .../definitions/Adt.py | 12 +- .../definitions/Wdt.py | 8 +- .../definitions/__init__.py | 0 .../definitions/chunks/MCLQ.py | 2 +- .../definitions/chunks/MCVT.py | 0 .../definitions/chunks/TileHeader.py | 0 .../definitions/chunks/TileInformation.py | 6 +- .../definitions/chunks/__init__.py | 0 .../definitions/enums/LiquidFlags.py | 0 .../definitions/enums/__init__.py | 0 .../definitions/reader/StreamReader.py | 0 .../definitions/reader/__init__.py | 0 .../helpers/Constants.py | 0 .../helpers/DataHolders.py | 0 .../helpers/HeightField.py | 2 +- .../helpers/LiquidAdtWriter.py | 4 +- .../helpers/__init__.py | 0 .../pydbclib/DbcReader.py | 2 +- .../pydbclib/__init__.py | 0 .../pydbclib/helpers/VanillaAreaHelper.py | 0 .../pydbclib/structs/AreaTable.py | 2 +- .../pydbclib/structs/DbcHeader.py | 0 .../pydbclib/structs/Map.py | 0 .../pympqlib/MpqArchive.py | 8 +- .../pympqlib/MpqEntry.py | 2 +- .../pympqlib/MpqFlags.py | 0 .../pympqlib/MpqHash.py | 0 .../pympqlib/MpqHeader.py | 0 .../pympqlib/MpqReader.py | 0 .../pympqlib/__init__.py | 0 utils/GitUtils.py | 44 ++++++- 39 files changed, 271 insertions(+), 65 deletions(-) create mode 100644 game/world/managers/maps/helpers/Mapbuild.py create mode 100644 tools/extractors/Extractor.py rename tools/{map_extractor => extractors}/MapExtractor.py (68%) create mode 100644 tools/extractors/NavExtractor.py rename tools/{map_extractor => extractors}/__init__.py (100%) rename tools/{map_extractor => extractors}/definitions/Adt.py (90%) rename tools/{map_extractor => extractors}/definitions/Wdt.py (92%) rename tools/{map_extractor => extractors}/definitions/__init__.py (100%) rename tools/{map_extractor => extractors}/definitions/chunks/MCLQ.py (90%) rename tools/{map_extractor => extractors}/definitions/chunks/MCVT.py (100%) rename tools/{map_extractor => extractors}/definitions/chunks/TileHeader.py (100%) rename tools/{map_extractor => extractors}/definitions/chunks/TileInformation.py (87%) rename tools/{map_extractor => extractors}/definitions/chunks/__init__.py (100%) rename tools/{map_extractor => extractors}/definitions/enums/LiquidFlags.py (100%) rename tools/{map_extractor => extractors}/definitions/enums/__init__.py (100%) rename tools/{map_extractor => extractors}/definitions/reader/StreamReader.py (100%) rename tools/{map_extractor => extractors}/definitions/reader/__init__.py (100%) rename tools/{map_extractor => extractors}/helpers/Constants.py (100%) rename tools/{map_extractor => extractors}/helpers/DataHolders.py (100%) rename tools/{map_extractor => extractors}/helpers/HeightField.py (98%) rename tools/{map_extractor => extractors}/helpers/LiquidAdtWriter.py (95%) rename tools/{map_extractor => extractors}/helpers/__init__.py (100%) rename tools/{map_extractor => extractors}/pydbclib/DbcReader.py (97%) rename tools/{map_extractor => extractors}/pydbclib/__init__.py (100%) rename tools/{map_extractor => extractors}/pydbclib/helpers/VanillaAreaHelper.py (100%) rename tools/{map_extractor => extractors}/pydbclib/structs/AreaTable.py (96%) rename tools/{map_extractor => extractors}/pydbclib/structs/DbcHeader.py (100%) rename tools/{map_extractor => extractors}/pydbclib/structs/Map.py (100%) rename tools/{map_extractor => extractors}/pympqlib/MpqArchive.py (96%) rename tools/{map_extractor => extractors}/pympqlib/MpqEntry.py (97%) rename tools/{map_extractor => extractors}/pympqlib/MpqFlags.py (100%) rename tools/{map_extractor => extractors}/pympqlib/MpqHash.py (100%) rename tools/{map_extractor => extractors}/pympqlib/MpqHeader.py (100%) rename tools/{map_extractor => extractors}/pympqlib/MpqReader.py (100%) rename tools/{map_extractor => extractors}/pympqlib/__init__.py (100%) 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/main.py b/main.py index b24ae85c3..1c1a9684d 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from game.world.managers.CommandManager import CommandManager from game.world.managers.maps.MapManager import MapManager from game.world.managers.maps.MapTile import MapTile -from tools.map_extractor.MapExtractor import MapExtractor +from tools.extractors.Extractor import Extractor from utils.ConfigManager import config, ConfigManager from utils.Logger import Logger from utils.PathManager import PathManager @@ -133,7 +133,7 @@ def wait_update_server(): exit() if args.extract: - MapExtractor.run() + Extractor.run() exit() # Validate if maps available and if version match. @@ -141,6 +141,10 @@ def wait_update_server(): Logger.error(f'Invalid maps version or maps missing, expected version {MapTile.EXPECTED_VERSION}') exit() + if not MapManager.validate_namigator_bindings(): + Logger.error(f'Invalid namigator bindings.') + exit() + # Semaphore objects are leaked on shutdown in macOS if using spawn for some reason. if platform == 'darwin': context = multiprocessing.get_context('fork') 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..8d9b8bc1f --- /dev/null +++ b/tools/extractors/NavExtractor.py @@ -0,0 +1,107 @@ +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 = 1 + 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) + return + except: + Logger.warning(traceback.format_exc()) + + @staticmethod + def _show_progress(process, map_name): + total = NavExtractor.maps_navs[map_name] + 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. + progress = NavExtractor._get_progress(map_name) + if progress: + 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/__init__.py b/tools/extractors/__init__.py similarity index 100% rename from tools/map_extractor/__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/__init__.py b/tools/extractors/definitions/__init__.py similarity index 100% rename from tools/map_extractor/definitions/__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/definitions/chunks/__init__.py b/tools/extractors/definitions/chunks/__init__.py similarity index 100% rename from tools/map_extractor/definitions/chunks/__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/definitions/enums/__init__.py b/tools/extractors/definitions/enums/__init__.py similarity index 100% rename from tools/map_extractor/definitions/enums/__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/definitions/reader/__init__.py b/tools/extractors/definitions/reader/__init__.py similarity index 100% rename from tools/map_extractor/definitions/reader/__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/map_extractor/helpers/__init__.py b/tools/extractors/helpers/__init__.py similarity index 100% rename from tools/map_extractor/helpers/__init__.py rename to tools/extractors/helpers/__init__.py 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/map_extractor/pydbclib/__init__.py b/tools/extractors/pydbclib/__init__.py similarity index 100% rename from tools/map_extractor/pydbclib/__init__.py rename to tools/extractors/pydbclib/__init__.py 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/map_extractor/pympqlib/__init__.py b/tools/extractors/pympqlib/__init__.py similarity index 100% rename from tools/map_extractor/pympqlib/__init__.py rename to tools/extractors/pympqlib/__init__.py 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() From c779449bcf6f5c47b4722f41ffb35fb7db583d8f Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sat, 22 Mar 2025 18:42:29 -0600 Subject: [PATCH 44/46] Update NavExtractor.py --- tools/extractors/NavExtractor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/extractors/NavExtractor.py b/tools/extractors/NavExtractor.py index 8d9b8bc1f..570b5fcb4 100644 --- a/tools/extractors/NavExtractor.py +++ b/tools/extractors/NavExtractor.py @@ -69,6 +69,7 @@ def run(data_path): @staticmethod def _show_progress(process, map_name): total = NavExtractor.maps_navs[map_name] + progress = 0 while process.is_alive(): progress = NavExtractor._get_progress(map_name) if progress: @@ -76,8 +77,8 @@ def _show_progress(process, map_name): sleep(1) # Final progress. - progress = NavExtractor._get_progress(map_name) - if 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 From addc298a97a365a1f52dbc1ef186e2a327543c81 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:38:36 -0600 Subject: [PATCH 45/46] Update NavExtractor.py --- tools/extractors/NavExtractor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/extractors/NavExtractor.py b/tools/extractors/NavExtractor.py index 570b5fcb4..c7fc7fed2 100644 --- a/tools/extractors/NavExtractor.py +++ b/tools/extractors/NavExtractor.py @@ -62,7 +62,6 @@ def run(data_path): # Wait for process. NavExtractor._show_progress(process, map_name) - return except: Logger.warning(traceback.format_exc()) From 3a3e20854b901555a7e1ea33465c5539c26f004f Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Mon, 24 Mar 2025 00:45:47 -0600 Subject: [PATCH 46/46] Update NavExtractor.py --- tools/extractors/NavExtractor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/extractors/NavExtractor.py b/tools/extractors/NavExtractor.py index c7fc7fed2..314ab7510 100644 --- a/tools/extractors/NavExtractor.py +++ b/tools/extractors/NavExtractor.py @@ -45,7 +45,7 @@ def run(data_path): os.mkdir(nav_path) try: - threads = 1 + threads = int(input("Number of threads?:")) Logger.info('[NavExtractor] Building bhv files...') NavExtractor._extract_bhv(data_path, mapbuild) @@ -67,6 +67,7 @@ def run(data_path): @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():