├── run.py ├── mapy ├── database │ ├── __init__.py │ ├── errors.py │ ├── types.py │ ├── structure.py │ └── db_client.py ├── scripts │ └── npc │ │ └── default.py ├── crypto │ ├── __init__.py │ ├── aes.py │ ├── shanda.py │ └── maple_iv.py ├── __init__.py ├── game │ ├── __init__.py │ ├── skill.py │ ├── inventory.py │ ├── item.py │ ├── field.py │ └── character.py ├── http_api.py ├── tools.py ├── logger.py ├── constants.py ├── packet.py ├── abstract.py ├── scripting.py ├── client.py ├── cpacket.py ├── opcodes.py └── server.py ├── _config.yml ├── screenshot.png ├── .flake8 ├── .pre-commit-config.yaml ├── .gitignore ├── .yamllint.yaml ├── .github └── workflows │ └── flake8.yml ├── README.md ├── pyproject.toml └── requirements.txt /run.py: -------------------------------------------------------------------------------- 1 | import mapy 2 | -------------------------------------------------------------------------------- /mapy/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rooba/mapy/HEAD/screenshot.png -------------------------------------------------------------------------------- /mapy/scripts/npc/default.py: -------------------------------------------------------------------------------- 1 | await ctx.say(f"Npc ID [{ctx.npc_id}] My script is not yet made") 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | E203 4 | W503 5 | max-line-length = 88 6 | exclude = mapy/scripts/*/*.py -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.3.0 4 | hooks: 5 | - id: black -------------------------------------------------------------------------------- /mapy/database/errors.py: -------------------------------------------------------------------------------- 1 | from asyncpg import PostgresError 2 | 3 | 4 | class SchemaError(PostgresError): 5 | ... 6 | 7 | 8 | class ResponseError(PostgresError): 9 | ... 10 | 11 | 12 | class QueryError(PostgresError): 13 | ... 14 | -------------------------------------------------------------------------------- /mapy/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | from .aes import MapleAes 2 | from .maple_iv import MapleIV 3 | from .shanda import decrypt_transform, encrypt_transform, roll_left, roll_right 4 | 5 | __all__ = ( 6 | "MapleIV", 7 | "MapleAes", 8 | "decrypt_transform", 9 | "encrypt_transform", 10 | "roll_left", 11 | "roll_right", 12 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # environment / pycache 2 | **/.git 3 | **/.vscode 4 | **/.vs 5 | **/*.pyc 6 | **/__pycache__ 7 | **/.DS_STORE 8 | **/.mypy_cache 9 | **/.venv 10 | 11 | **/notes.md 12 | **/config.* 13 | **/test*.* 14 | **/http_db_client.py 15 | **/tests 16 | **/.style.yapf 17 | **/venv/ 18 | **/.pytest_cache/ 19 | **/.idea/ 20 | mini_shanda.py 21 | config_test.yaml 22 | __config_test.yaml 23 | config.yaml 24 | mapy.sql 25 | **/.idea/** 26 | poetry.lock 27 | -------------------------------------------------------------------------------- /mapy/__init__.py: -------------------------------------------------------------------------------- 1 | from . import server 2 | from . import constants, types 3 | 4 | from .cpacket import CPacket 5 | from .logger import Logger 6 | from .opcodes import CRecvOps, CSendOps 7 | from .packet import ByteBuffer, Packet, packet_handler 8 | 9 | 10 | __all__ = ( 11 | "server", "constants", 12 | "CPacket", 13 | "Logger", 14 | "Packet", 15 | "packet_handler", 16 | "ByteBuffer", 17 | "CRecvOps", 18 | "CSendOps", 19 | "types", 20 | ) 21 | -------------------------------------------------------------------------------- /mapy/game/__init__.py: -------------------------------------------------------------------------------- 1 | from .character import CharacterEntry, FuncKey, MapleCharacter, SkillEntry, Account 2 | from .field import Field, Foothold, Mob, Npc, Portal 3 | from .item import ItemSlotBundle, ItemSlotEquip 4 | from .skill import SkillLevelData 5 | 6 | 7 | __all__ = ( 8 | "Account", 9 | "ItemSlotEquip", 10 | "ItemSlotBundle", 11 | "MapleCharacter", 12 | "CharacterEntry", 13 | "FuncKey", 14 | "SkillEntry", 15 | "Foothold", 16 | "Mob", 17 | "Npc", 18 | "Portal", 19 | "Field", 20 | "SkillLevelData", 21 | ) 22 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | braces: 7 | level: warning 8 | max-spaces-inside: 1 9 | brackets: 10 | level: warning 11 | max-spaces-inside: 1 12 | colons: 13 | level: warning 14 | commas: 15 | level: warning 16 | comments: disable 17 | comments-indentation: disable 18 | document-start: disable 19 | empty-lines: 20 | level: warning 21 | hyphens: 22 | level: warning 23 | indentation: 24 | level: warning 25 | indent-sequences: consistent 26 | line-length: 27 | level: warning 28 | allow-non-breakable-inline-mappings: true 29 | truthy: disable -------------------------------------------------------------------------------- /.github/workflows/flake8.yml: -------------------------------------------------------------------------------- 1 | name: flake8 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install flake8 21 | - name: Analysing the code with flake8 22 | run: | 23 | flake8 $(git ls-files '*.py') 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MaPy 2 | 3 | Maplestory emulator written in Python 4 | 5 | ## Requirements 6 | 7 | - Postgresql 8 | - Python 3.10+ 9 | - pycryptodome 10 | - asyncpg 11 | - attrs 12 | - more-itertools 13 | - jinja2 14 | - fastapi 15 | - uvicorn 16 | - pyyaml 17 | 18 | ## Optional 19 | 20 | - itsdangerous 21 | - pydantic 22 | - loguru 23 | - uvloop 24 | 25 | ## To-Do 26 | 27 | - Multi-Version Compat 28 | 29 | - Template out packet handlers, opcodes, game objects, and client to have drop-in's per version 30 | - Map objects to relational tables for database generation 31 | 32 | - Inventory Operations 33 | 34 | - Move item slot 35 | - Drop Items 36 | - Loot Items 37 | - Proper serial handling for equips when transferring ownership 38 | - Increase/Decrease stack count 39 | 40 | - Portal Navigation 41 | 42 | - Properly handle mob spawn / idle upon no map owner / despawn upon expiration 43 | - Field ground item lifetime 44 | 45 | - Consumables 46 | 47 | - Potions 48 | - Equipment scrolls 49 | - Teleport scrolls 50 | - Follower consumables 51 | - Cash Effects 52 | - Chairs 53 | 54 | - Followers (Pets, Shadow Partner, Mage summons, Archer Summons, Beholder) 55 | 56 | - Move Path 57 | - Field registration 58 | - Expiration 59 | 60 | - Secondary Stat (Buff Stats) 61 | -------------------------------------------------------------------------------- /mapy/game/skill.py: -------------------------------------------------------------------------------- 1 | from attrs import define, field 2 | 3 | 4 | class Skill: 5 | def __init__(self, id): 6 | self._id = id 7 | self._skill_level_data = [] 8 | 9 | 10 | class SkillLevel: 11 | def __init__(self, **kwargs): 12 | for key, value in kwargs.items(): 13 | setattr(self, key, value) 14 | 15 | 16 | @define 17 | class SkillLevelData(object): 18 | flags: list[int] = field(factory=list) 19 | weapon: int = 0 20 | sub_weapon: int = 0 21 | max_level: int = 0 22 | base_max_level: int = 0 23 | skill_type: list = field(factory=list) 24 | element: str = field(factory=str) 25 | mob_count: str = field(factory=str) 26 | hit_count: str = field(factory=str) 27 | buff_time: str = field(factory=str) 28 | mp_cost: str = field(factory=str) 29 | hp_cost: str = field(factory=str) 30 | damage: str = field(factory=str) 31 | fixed_damage: str = field(factory=str) 32 | critical_damage: str = field(factory=str) 33 | _levels: dict = field(factory=dict) 34 | mastery: str = field(factory=str) 35 | 36 | def __post_init__(self): 37 | self._levels = {} 38 | for i in range(self.max_level): 39 | kwargs = {} 40 | for name, value in self.__dict__.items(): 41 | if isinstance(value, str): 42 | # kwargs[name] = rtl_equation(value, i) 43 | pass 44 | else: 45 | kwargs[name] = value 46 | 47 | self._levels[i] = SkillLevel(**kwargs) 48 | 49 | def __getitem__(self, index): 50 | return self._levels[index] 51 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | 4 | [project] 5 | authors = [{email = "ra@tcp.direct"}, {name = "ra"}] 6 | classifiers = ["Development Status :: 0.4.1 Dev"] 7 | dependencies = [ 8 | "attrs>=21.4.0", 9 | "loguru>=0.6.0", 10 | "pycryptodomex>=3.14.1", 11 | ] 12 | description = "MaPy MapleStory Emulator" 13 | keywords = ["MapleStory", "Server Emulator"] 14 | maintainers = [{name = "ra", email = "ra@tcp.direct"}] 15 | name = "MaPy" 16 | readme = "README.md" 17 | requires-python = "^3.10.2" 18 | version = "0.4.1" 19 | 20 | [project.optional-dependencies] 21 | linux = ["uvloop>=0.16.0"] 22 | 23 | [tool.flake8] 24 | exclude = "mapy/scripts/*/*.py" 25 | extend-ignore = ["E203", "E501"] 26 | max-line-length = 88 27 | 28 | [tool.black] 29 | include = "\\.pyi?$" 30 | skip-magic-trailing-comma = true 31 | target-version = ["py310"] 32 | line-length = 88 33 | 34 | [tool.poetry] 35 | description = "MaPy MapleStory Emulator" 36 | name = "MaPy" 37 | version = "0.0.1" 38 | 39 | license = "MIT" 40 | 41 | authors = ["ra "] 42 | 43 | readme = "README.md" 44 | 45 | homepage = "https://github.com/Rooba/mapy" 46 | repository = "https://github.com/Rooba/mapy" 47 | 48 | keywords = ["MapleStory", "Server Emulator"] 49 | 50 | [tool.poetry.dependencies] 51 | python = "^3.10" # Compatible python versions must be declared here 52 | toml = "^0.10.2" 53 | # Dependencies with extras 54 | requests = {version = "^2.13", extras = ["security"]} 55 | # Python specific dependencies with prereleases allowed 56 | pathlib2 = {version = "^2.2", python = "~2.7", allow-prereleases = true} 57 | # Git dependencies 58 | cleo = {version = "~0.8.1"} 59 | 60 | PyYAML = ">=6.0" 61 | attrs = ">=21.4.0" 62 | cryptography = ">=36.0.2" 63 | loguru = ">=0.6.0" 64 | pycryptodomex = ">=3.14.1" 65 | pyright = ">=1.1.237" 66 | yarl = ">=1.7.0" 67 | uvloop = {version = ">=0.16.0", optional = true } 68 | yapf = {version = ">=0.32.0", optional = true } 69 | httptools = ">=0.4.0" 70 | websockets = ">=10.3" 71 | asyncpg = "^0.26.0" 72 | sanic = "^22.6.2" 73 | 74 | -------------------------------------------------------------------------------- /mapy/http_api.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | from re import compile 3 | 4 | from sanic import Blueprint, Sanic, json 5 | from sanic.blueprint_group import BlueprintGroup 6 | 7 | mem_re = compile(r"^[A-Z_]+$") 8 | 9 | http_api = Sanic("__mapy__") 10 | http_api.enable_websocket() 11 | http_api.asgi = True 12 | api = Blueprint(name="api", url_prefix="api/") 13 | ws = Blueprint(name="ws", url_prefix="ws/") 14 | api_group = BlueprintGroup("") 15 | api_group.extend([api, ws]) 16 | 17 | 18 | @api.websocket("/ws/status/") 19 | async def ws_stats(request, ws): 20 | _center = request.app.ctx.center 21 | login = _center._login 22 | _stat = { 23 | "uptime": _center.uptime, 24 | "population": _center.population, 25 | "login_server": { 26 | "alive": login.alive if login else 0, 27 | "port": login.port if login else 0, 28 | "population": login.population if login else 0, 29 | }, 30 | "game_servers": { 31 | world.name: { 32 | f"{i}": { 33 | "alive": channel.alive, 34 | "port": channel.port, 35 | "population": channel.population, 36 | } 37 | for i, channel in enumerate(world.channels, 1) 38 | } 39 | for world in _center.worlds.values() 40 | }, 41 | } 42 | if ws.data_received: 43 | content = await ws.recv_burst() 44 | print(content) 45 | await ws.send(dumps(_stat)) 46 | await ws.close() 47 | 48 | 49 | @api.get("/status/") 50 | async def statistics(request): 51 | _center = request.app.ctx.center 52 | login = _center._login 53 | _stat = { 54 | "uptime": _center.uptime, 55 | "population": _center.population, 56 | "login_server": { 57 | "alive": login.alive if login else 0, 58 | "port": login.port if login else 0, 59 | "population": login.population if login else 0, 60 | }, 61 | "game_servers": { 62 | world.name: { 63 | i: { 64 | "alive": channel.alive, 65 | "port": channel.port, 66 | "population": channel.population, 67 | } 68 | for i, channel in enumerate(world.channels, 1) 69 | } 70 | for world in _center.worlds.values() 71 | }, 72 | } 73 | return json(_stat) 74 | 75 | 76 | http_api.blueprint(api_group) 77 | -------------------------------------------------------------------------------- /mapy/crypto/aes.py: -------------------------------------------------------------------------------- 1 | try: 2 | from Cryptodome.Cipher import AES # type: ignore 3 | except ImportError: 4 | from Crypto.Cipher import AES # type: ignore 5 | except ImportError: 6 | raise Exception("Please install pycryptodomex") 7 | 8 | # from struct import unpack, pack 9 | 10 | 11 | class MapleAes: 12 | _user_key = bytearray([ 13 | 0x13, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 14 | 0xB4, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 15 | 0x33, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00 16 | ]) 17 | 18 | @classmethod 19 | def transform(cls, buffer, iv): 20 | remaining = len(buffer) 21 | length = 0x5B0 22 | start = 0 23 | 24 | real_iv = bytearray(16) 25 | 26 | iv_bytes = [ 27 | iv.value & 255, 28 | iv.value >> 8 & 255, 29 | iv.value >> 16 & 255, 30 | iv.value >> 24 & 255, 31 | ] 32 | 33 | while remaining > 0: 34 | for index in range(len(real_iv)): 35 | real_iv[index] = iv_bytes[index % 4] 36 | 37 | if remaining < length: 38 | length = remaining 39 | 40 | index = start 41 | 42 | while index < start + length: 43 | sub = index - start 44 | 45 | if (sub % 16) == 0: 46 | real_iv = AES.new(cls._user_key, 47 | AES.MODE_ECB).encrypt(real_iv) 48 | 49 | buffer[index] ^= real_iv[sub % 16] 50 | index += 1 51 | 52 | start += length 53 | remaining -= length 54 | length = 0x5B4 55 | 56 | iv.shuffle() 57 | 58 | return buffer 59 | 60 | # @staticmethod 61 | # def get_header(data, iv, length, major_ver): 62 | # first = -(major_ver + 1) ^ iv.hiword 63 | # a = first & 0xFF 64 | # b = first >> 8 & 0xFF 65 | # second = (a & 0xFF | b << 8 & 0xFF00) ^ length 66 | # c = second & 0xFF 67 | # d = second >> 8 & 0xFF 68 | # data[0:4] = bytearray([a, b, c, d]) 69 | # return data 70 | 71 | @staticmethod 72 | def get_header(data, iv, length, major_ver): 73 | first = -(major_ver + 1) ^ iv.hiword 74 | second = (first + 2**16) ^ length 75 | data[0:2] = bytes([first & 0xFF, first >> 8 & 0xFF]) 76 | data[2:4] = bytes([second & 0xFF, second >> 8 & 0xFF]) 77 | 78 | @staticmethod 79 | def get_length(data): 80 | return ((data[1] << 8) + data[0]) ^ ((data[3] << 8) + data[2]) 81 | -------------------------------------------------------------------------------- /mapy/crypto/shanda.py: -------------------------------------------------------------------------------- 1 | def decrypt_transform(data): 2 | for j in range(1, 7): 3 | remember = 0 4 | data_length = len(data) & 0xFF 5 | next_remember = 0 6 | if j % 2 == 0: 7 | for i in range(len(data)): 8 | cur = data[i] 9 | cur = (cur - 0x48) & 0xFF 10 | cur = ~cur & 0xFF 11 | cur = roll_left(cur, data_length & 0xFF) 12 | next_remember = cur 13 | cur ^= remember 14 | remember = next_remember 15 | cur = (cur - data_length) & 0xFF 16 | cur = roll_right(cur, 3) 17 | data[i] = cur 18 | data_length -= 1 19 | else: 20 | for i in reversed(range(len(data))): 21 | cur = data[i] 22 | cur = roll_left(cur, 3) 23 | cur ^= 0x13 24 | next_remember = cur 25 | cur ^= remember 26 | remember = next_remember 27 | cur = (cur - data_length) & 0xFF 28 | cur = roll_right(cur, 4) & 0xFF 29 | data[i] = cur 30 | data_length -= 1 31 | 32 | return data 33 | 34 | 35 | def encrypt_transform(data): 36 | b = {str(i): 0 for i in range(len(data))} 37 | cur = 0 38 | 39 | for _ in range(3): 40 | length = len(data) & 0xFF 41 | xor_key = 0 42 | i = 0 43 | while i < len(data): 44 | 45 | cur = roll_left(data[i], 3) 46 | cur = cur + length 47 | cur = (cur ^ xor_key) & 0xFF 48 | xor_key = cur 49 | cur = ~roll_right(cur, length) & 0xFF 50 | cur = (cur + 0x48) & 0xFF 51 | data[i] = cur 52 | b[str(i)] = cur 53 | length -= 1 54 | i += 1 55 | 56 | xor_key = 0 57 | length = len(data) & 0xFF 58 | i = len(data) - 1 59 | 60 | while i >= 0: 61 | cur = roll_left(data[i], 4) 62 | cur += length 63 | cur = (cur ^ xor_key) & 0xFF 64 | xor_key = cur 65 | cur ^= 0x13 66 | cur = roll_right(cur, 3) 67 | data[i] = cur 68 | b[str(i)] = cur 69 | length -= 1 70 | i -= 1 71 | 72 | return bytearray([b[b_] for b_ in b]) 73 | 74 | 75 | def roll_left(value, shift): 76 | num = value << (shift % 8) 77 | return (num & 0xFF) | (num >> 8) 78 | 79 | 80 | def roll_right(value, shift): 81 | num = (value << 8) >> (shift % 8) 82 | return (num & 0xFF) | (num >> 8) 83 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.8.0; python_version >= "3.7" and python_version < "4.0" 2 | asyncpg==0.26.0; python_full_version >= "3.6.0" 3 | attrs==22.1.0; python_version >= "3.5" 4 | certifi==2022.6.15; python_version >= "3.7" and python_version < "4" 5 | cffi==1.15.1; python_version >= "3.6" 6 | charset-normalizer==2.1.1; python_version >= "3.7" and python_version < "4" and python_full_version >= "3.6.0" 7 | cleo==0.8.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") 8 | clikit==0.6.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" 9 | colorama==0.4.5; python_version >= "3.5" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.5" and python_full_version >= "3.5.0" 10 | crashtest==0.3.1; python_version >= "3.6" and python_version < "4.0" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") 11 | cryptography==38.0.3; python_version >= "3.6" 12 | httptools==0.4.0; python_full_version >= "3.5.0" 13 | idna==3.3; python_version >= "3.7" and python_version < "4" 14 | loguru==0.6.0; python_version >= "3.5" 15 | multidict==6.0.2; python_version >= "3.7" 16 | nodeenv==1.7.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" and python_version >= "3.7" 17 | pastel==0.2.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" 18 | pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" 19 | pycryptodomex==3.15.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") 20 | pylev==1.4.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" 21 | pyright==1.1.269; python_version >= "3.7" 22 | pyyaml==6.0; python_version >= "3.6" 23 | requests==2.28.1; python_version >= "3.7" and python_version < "4" 24 | sanic-routing==22.3.0; python_version >= "3.7" 25 | sanic==22.6.2; python_version >= "3.7" 26 | toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") 27 | ujson==5.4.0; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.7" 28 | urllib3==1.26.12; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" 29 | uvloop==0.16.0; python_version >= "3.7" 30 | websockets==10.3; python_version >= "3.7" 31 | win32-setctime==1.1.0; sys_platform == "win32" and python_version >= "3.5" 32 | yarl==1.8.1; python_version >= "3.7" 33 | -------------------------------------------------------------------------------- /mapy/tools.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | from dataclasses import dataclass, is_dataclass 3 | from random import randint 4 | 5 | 6 | class TagPoint: 7 | def __init__(self, x=0, y=0): 8 | self.x = x 9 | self.y = y 10 | 11 | def __str__(self): 12 | return f"{self.x},{self.y}" 13 | 14 | 15 | class Random: 16 | def __init__(self): 17 | self.seed_1 = randint(1, 2**31 - 1) 18 | self.seed_2 = randint(1, 2**31 - 1) 19 | self.seed_3 = randint(1, 2**31 - 1) 20 | 21 | def encode(self, packet): 22 | packet.encode_int(self.seed_1) 23 | packet.encode_int(self.seed_2) 24 | packet.encode_int(self.seed_3) 25 | 26 | 27 | def find(predicate, seq): 28 | for element in seq: 29 | if predicate(element): 30 | return element 31 | return None 32 | 33 | 34 | def get(iterable, **attrs): 35 | def predicate(elem): 36 | for attr, val in attrs.items(): 37 | nested = attr.split("__") 38 | obj = elem 39 | for attribute in nested: 40 | obj = getattr(obj, attribute) 41 | 42 | if obj != val: 43 | return False 44 | return True 45 | 46 | return find(predicate, iterable) 47 | 48 | 49 | def filter_out_to(func, to_filter, out): 50 | new = [] 51 | 52 | for item in to_filter: 53 | if func(item): 54 | new.append(item) 55 | else: 56 | out.append(item) 57 | 58 | return new 59 | 60 | 61 | def first_or_default(list_, f): 62 | return next((val for val in list_ if f(val)), None) 63 | 64 | 65 | def fix_dict_keys(dict_): 66 | copy = dict(dict_) 67 | 68 | for key, value in copy.items(): 69 | if key.isdigit(): 70 | value = dict_.pop(key) 71 | key = int(key) 72 | 73 | if isinstance(value, dict): 74 | dict_[key] = fix_dict_keys(value) 75 | 76 | else: 77 | dict_[key] = value 78 | 79 | return dict_ 80 | 81 | 82 | def to_string(bytes_): 83 | return " ".join( 84 | [bytes_.hex()[i : i + 2].upper() for i in range(0, len(bytes_.hex()), 2)] 85 | ) 86 | 87 | 88 | async def wakeup(): 89 | while True: 90 | await sleep(600) 91 | 92 | 93 | def nested_dataclass(*args, **kwargs): 94 | def wrapper(cls): 95 | cls = dataclass(**kwargs)(cls) 96 | original_init = cls.__init__ 97 | 98 | def __init__(self, *args_, **kwargs_): 99 | for name, value in kwargs_.items(): 100 | field_type = cls.__annotations__.get(name, None) 101 | 102 | if is_dataclass(field_type) and isinstance(value, dict): 103 | new_obj = field_type(**value) 104 | kwargs_[name] = new_obj 105 | 106 | original_init(self, *args_, **kwargs_) 107 | 108 | cls.__init__ = __init__ 109 | return cls 110 | 111 | return wrapper(args[0]) if args else wrapper 112 | 113 | 114 | class Manager(list): 115 | def get(self, search): 116 | return first_or_default(self, search) 117 | 118 | def first_or_default(self, func): 119 | return next((val for val in self if func(val)), None) 120 | -------------------------------------------------------------------------------- /mapy/crypto/maple_iv.py: -------------------------------------------------------------------------------- 1 | class MapleIV: 2 | _shuffle = bytearray([ 3 | 0xEC, 0x3F, 0x77, 0xA4, 0x45, 0xD0, 0x71, 0xBF, 0xB7, 0x98, 0x20, 0xFC, 4 | 0x4B, 0xE9, 0xB3, 0xE1, 0x5C, 0x22, 0xF7, 0x0C, 0x44, 0x1B, 0x81, 0xBD, 5 | 0x63, 0x8D, 0xD4, 0xC3, 0xF2, 0x10, 0x19, 0xE0, 0xFB, 0xA1, 0x6E, 0x66, 6 | 0xEA, 0xAE, 0xD6, 0xCE, 0x06, 0x18, 0x4E, 0xEB, 0x78, 0x95, 0xDB, 0xBA, 7 | 0xB6, 0x42, 0x7A, 0x2A, 0x83, 0x0B, 0x54, 0x67, 0x6D, 0xE8, 0x65, 0xE7, 8 | 0x2F, 0x07, 0xF3, 0xAA, 0x27, 0x7B, 0x85, 0xB0, 0x26, 0xFD, 0x8B, 0xA9, 9 | 0xFA, 0xBE, 0xA8, 0xD7, 0xCB, 0xCC, 0x92, 0xDA, 0xF9, 0x93, 0x60, 0x2D, 10 | 0xDD, 0xD2, 0xA2, 0x9B, 0x39, 0x5F, 0x82, 0x21, 0x4C, 0x69, 0xF8, 0x31, 11 | 0x87, 0xEE, 0x8E, 0xAD, 0x8C, 0x6A, 0xBC, 0xB5, 0x6B, 0x59, 0x13, 0xF1, 12 | 0x04, 0x00, 0xF6, 0x5A, 0x35, 0x79, 0x48, 0x8F, 0x15, 0xCD, 0x97, 0x57, 13 | 0x12, 0x3E, 0x37, 0xFF, 0x9D, 0x4F, 0x51, 0xF5, 0xA3, 0x70, 0xBB, 0x14, 14 | 0x75, 0xC2, 0xB8, 0x72, 0xC0, 0xED, 0x7D, 0x68, 0xC9, 0x2E, 0x0D, 0x62, 15 | 0x46, 0x17, 0x11, 0x4D, 0x6C, 0xC4, 0x7E, 0x53, 0xC1, 0x25, 0xC7, 0x9A, 16 | 0x1C, 0x88, 0x58, 0x2C, 0x89, 0xDC, 0x02, 0x64, 0x40, 0x01, 0x5D, 0x38, 17 | 0xA5, 0xE2, 0xAF, 0x55, 0xD5, 0xEF, 0x1A, 0x7C, 0xA7, 0x5B, 0xA6, 0x6F, 18 | 0x86, 0x9F, 0x73, 0xE6, 0x0A, 0xDE, 0x2B, 0x99, 0x4A, 0x47, 0x9C, 0xDF, 19 | 0x09, 0x76, 0x9E, 0x30, 0x0E, 0xE4, 0xB2, 0x94, 0xA0, 0x3B, 0x34, 0x1D, 20 | 0x28, 0x0F, 0x36, 0xE3, 0x23, 0xB4, 0x03, 0xD8, 0x90, 0xC8, 0x3C, 0xFE, 21 | 0x5E, 0x32, 0x24, 0x50, 0x1F, 0x3A, 0x43, 0x8A, 0x96, 0x41, 0x74, 0xAC, 22 | 0x52, 0x33, 0xF0, 0xD9, 0x29, 0x80, 0xB1, 0x16, 0xD3, 0xAB, 0x91, 0xB9, 23 | 0x84, 0x7F, 0x61, 0x1E, 0xCF, 0xC5, 0xD1, 0x56, 0x3D, 0xCA, 0xF4, 0x05, 24 | 0xC6, 0xE5, 0x08, 0x49 25 | ]) 26 | 27 | def __init__(self, vector): 28 | self.value = vector 29 | 30 | def __int__(self): 31 | return self.value 32 | 33 | @property 34 | def hiword(self): 35 | return self.value >> 16 36 | 37 | @property 38 | def loword(self): 39 | return self.value 40 | 41 | def shuffle(self): 42 | seed = [0xF2, 0x53, 0x50, 0xC6] 43 | p_iv = self.value 44 | 45 | for i in range(4): 46 | temp_p_iv = (p_iv >> (8 * i)) & 0xFF 47 | 48 | a = seed[1] 49 | b = a 50 | b = self._shuffle[b & 0xFF] 51 | b -= temp_p_iv 52 | seed[0] += b 53 | b = seed[2] 54 | b ^= self._shuffle[int(temp_p_iv) & 0xFF] 55 | a -= int(b) & 0xFF 56 | seed[1] = a 57 | a = seed[3] 58 | b = a 59 | a -= seed[0] & 0xFF 60 | b = self._shuffle[b & 0xFF] 61 | b += temp_p_iv 62 | b ^= seed[2] 63 | seed[2] = b & 0xFF 64 | a += self._shuffle[temp_p_iv & 0xFF] & 0xFF 65 | seed[3] = a 66 | 67 | c = seed[0] & 0xFF 68 | c |= (seed[1] << 8) & 0xFFFF 69 | c |= (seed[2] << 16) & 0xFFFFFF 70 | c |= (seed[3] << 24) & 0xFFFFFFFF 71 | 72 | c = (c << 0x03) | (c >> 0x1D) 73 | 74 | seed[0] = c & 0xFF 75 | seed[1] = (c >> 8) & 0xFFFF 76 | seed[2] = (c >> 16) & 0xFFFFFF 77 | seed[3] = (c >> 24) & 0xFFFFFFFF 78 | 79 | c = seed[0] & 0xFF 80 | c |= (seed[1] << 8) & 0xFFFF 81 | c |= (seed[2] << 16) & 0xFFFFFF 82 | c |= (seed[3] << 24) & 0xFFFFFFFF 83 | 84 | self.value = c 85 | -------------------------------------------------------------------------------- /mapy/logger.py: -------------------------------------------------------------------------------- 1 | from re import I, X, compile 2 | from sys import stdout 3 | from threading import Lock 4 | 5 | from loguru._logger import Core 6 | from loguru._logger import Logger as _Logger, Level 7 | from loguru._colorizer import Colorizer 8 | 9 | from .constants import Worlds 10 | 11 | PACKET_RE = compile(r"(?P[\w\d._]+)\s(?P[\d.]+)\s(?P[A-Z\d\s]*)") 12 | SERVER_RE = compile( 13 | r"""^ 14 | (?P 15 | (?P[a-zA-Z]+\s?[a-zA-Z]+?) 16 | (?P 17 | \[(?P\d+)] 18 | \[(?P\d+)] 19 | )? 20 | )\s 21 | (?P.+)$""", 22 | flags=I | X, 23 | ) 24 | 25 | 26 | def pkt_fmt(bound): 27 | def _fmt_packet(rec): 28 | match_packet = PACKET_RE.search(rec["message"]) 29 | if not match_packet: 30 | op_code, ip, packet = "Unknown" * 3 31 | else: 32 | op_code, ip, packet = list(match_packet.group(1, 2, 3)) 33 | 34 | string = ( 35 | f"[{bound}] " 36 | f"[{op_code}] " 37 | f"[{ip}] {packet}" 38 | "\n" 39 | ) 40 | return string 41 | 42 | return _fmt_packet 43 | 44 | 45 | def fmt_basic(record): 46 | owner = record["extra"]["owner"] 47 | lvl = record["level"].name.title() 48 | 49 | if owner.__class__.__name__ == "WvsGame": 50 | world_name = Worlds(owner.world_id).name.replace("_", " ").title() 51 | channel_id = owner.channel_id 52 | nam = f"{world_name}:{channel_id}" 53 | srv = f"[{nam: <18}] " 54 | name = f"{f'[{lvl}]': <41}" f"{srv: <47}" 55 | 56 | else: 57 | srv_name = owner.__class__.__name__ 58 | name = ( 59 | f"{f'[{lvl}]': <41}" 60 | f"{f'[{srv_name: <9}] ': <38}" 61 | ) 62 | 63 | return f"{name}{record['message']}\n" 64 | 65 | 66 | ipkt = Level("IN PACKET", 51, "", None) 67 | opkt = Level("OUT PACKET", 52, "", None) 68 | basic = Level("BASIC", 11, "", None) 69 | 70 | 71 | class Logger(_Logger): 72 | __core = Core() 73 | __pre_init__ = False 74 | __init_lock__ = Lock() 75 | 76 | with __core.lock: 77 | for v in [ipkt, opkt, basic]: 78 | __core.levels[v.name] = v 79 | __core.levels_ansi_codes[v.name] = Colorizer.ansify(v.color) 80 | for handler in __core.handlers.values(): 81 | handler.update_format(v.name) 82 | 83 | def __init__(self, owner): 84 | super().__init__( 85 | self.__core, 86 | None, 87 | 0, 88 | False, 89 | False, 90 | False, 91 | False, 92 | True, 93 | None, 94 | {"owner": owner}, 95 | ) 96 | 97 | with Logger.__init_lock__: 98 | if not Logger.__pre_init__: 99 | self.add( 100 | stdout, level=ipkt.no, colorize=True, format=pkt_fmt("IN PACKET") 101 | ) 102 | self.add( 103 | stdout, level=opkt.no, colorize=True, format=pkt_fmt("OUT PACKET") 104 | ) 105 | self.add( 106 | stdout, 107 | level=basic.no, 108 | format=fmt_basic, 109 | colorize=True, 110 | diagnose=True, 111 | ) 112 | Logger.__pre_init__ = True 113 | 114 | def log_ipkt(self, message): 115 | return self._log(ipkt.name, ipkt.no, False, self._options, message, (), {}) 116 | 117 | def log_opkt(self, message): 118 | return self._log(opkt.name, opkt.no, False, self._options, message, (), {}) 119 | 120 | def log_basic(self, message): 121 | return self._log(basic.name, basic.no, False, self._options, message, (), {}) 122 | -------------------------------------------------------------------------------- /mapy/constants.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class Worlds(IntEnum): 5 | SCANIA = 0 6 | BROA = 1 7 | WINDIA = 2 8 | KHAINI = 3 9 | BELLOCAN = 4 10 | MARDIA = 5 11 | KRADIA = 6 12 | YELLONDE = 7 13 | GALACIA = 8 14 | EL_NIDO = 9 15 | ZENITH = 11 16 | ARCENIA = 12 17 | JUDIS = 13 18 | PLANA = 14 19 | KASTIA = 15 20 | KALLUNA = 16 21 | STIUS = 17 22 | CROA = 18 23 | MEDERE = 19 24 | 25 | 26 | class Network: 27 | __registered_channels__ = {} 28 | 29 | HOST_IP = "127.0.0.1" 30 | SERVER_ADDRESS = bytearray([127, 0, 0, 1]) 31 | 32 | LOGIN_PORT = 8484 33 | __GAME_PORT = 8585 34 | SHOP_PORT = 8787 35 | DATABASE_TYPE = "postgres" # postgres / mariadb / mysql 36 | USE_DATABASE = False 37 | CHANNEL_COUNT = 3 38 | ACTIVE_WORLDS = [Worlds.BELLOCAN, Worlds.EL_NIDO, Worlds.GALACIA] 39 | 40 | USE_HTTP_API = True 41 | HTTP_API_ROUTE = "/api" 42 | HTTP_HOST = "127.0.0.1", 8080 43 | STATISTICS = True 44 | 45 | @classmethod 46 | @property 47 | def GAME_PORT(cls): 48 | cls.__GAME_PORT += 1 49 | return cls.__GAME_PORT 50 | 51 | 52 | class Config: 53 | 54 | VERSION = 95 55 | SUB_VERSION = "1" 56 | LOCALE = 8 57 | 58 | WORLD_COUNT = 1 59 | CHANNEL_COUNT = 4 60 | 61 | EXP_RATE = 1 62 | QUEST_EXP = 1 63 | PARTY_QUEST_EXP = 1 64 | MESO_RATE = 1 65 | DROP_RATE = 1 66 | 67 | LOG_PACKETS = True 68 | 69 | AUTO_LOGIN = False 70 | AUTO_REGISTER = True 71 | REQUEST_PIN = False 72 | REQUEST_PIC = False 73 | REQUIRE_STAFF_IP = False 74 | MAX_CHARACTERS = 3 75 | 76 | DEFAULT_EVENT_MESSAGE = "Wow amazing world choose this one" 77 | DEFAULT_TICKER = "Welcome" 78 | ALLOW_MULTI_LEVELING = False 79 | DEFAULT_CREATION_SLOTS = 3 80 | DISABLE_CHARACTER_CREATION = True 81 | 82 | 83 | PERMANENT = 150841440000000000 84 | 85 | ANTIREPEAT_BUFFS = [ 86 | 11101004, 87 | 5221000, 88 | 11001001, 89 | 5211007, 90 | 5121000, 91 | 5121007, 92 | 5111007, 93 | 4341000, 94 | 5111007, 95 | 4121000, 96 | 4201003, 97 | 2121000, 98 | 1221000, 99 | 1201006, 100 | 1211008, 101 | 1211009, 102 | 1211010, 103 | 1121000, 104 | 1001003, 105 | 1101006, 106 | 1111007, 107 | 2101001, 108 | 2101003, 109 | 1321000, 110 | 1311007, 111 | 1311006, 112 | ] 113 | 114 | EVENT_VEHICLE_SKILLS = [ 115 | 1025, 116 | 1027, 117 | 1028, 118 | 1029, 119 | 1030, 120 | 1031, 121 | 1033, 122 | 1034, 123 | 1035, 124 | 1036, 125 | 1037, 126 | 1038, 127 | 1039, 128 | 1040, 129 | 1042, 130 | 1044, 131 | 1049, 132 | 1050, 133 | 1051, 134 | 1052, 135 | 1053, 136 | 1054, 137 | 1063, 138 | 1064, 139 | 1065, 140 | 1069, 141 | 1070, 142 | 1071, 143 | ] 144 | 145 | 146 | def is_event_vehicle_skill(skill_id): 147 | return skill_id % 10000 in EVENT_VEHICLE_SKILLS 148 | 149 | 150 | def get_job_from_creation(job_id): 151 | return {0: 3000, 1: 0, 2: 1000, 3: 2000, 4: 2001}.get(job_id, 0) 152 | 153 | 154 | def is_extend_sp_job(job_id): 155 | return job_id / 1000 == 3 or job_id / 100 == 22 or job_id == 2001 156 | 157 | 158 | class WorldFlag(IntEnum): 159 | Null = 0x00 160 | Event = 0x01 161 | New = 0x02 162 | Hot = 0x03 163 | 164 | 165 | class InventoryType(IntEnum): 166 | TRACKER = 0x0 167 | EQUIP = 0x1 168 | CONSUME = 0x2 169 | INSTALL = 0x3 170 | ETC = 0x4 171 | CASH = 0x5 172 | 173 | 174 | class ItemType(IntEnum): 175 | EQUIP = 10 176 | CONSUMABLE = 20 177 | RECHARGABLE = 21 178 | SETUP = 30 179 | ETC = 40 180 | CASH = 50 181 | PET = 51 182 | 183 | 184 | class StatModifiers(IntEnum): 185 | def __new__(cls, value: int, encode_type: str): 186 | cls.encode = encode_type 187 | obj = int.__new__(cls, value) 188 | obj._value_ = value 189 | return obj 190 | 191 | SKIN = (0x1, "byte") 192 | FACE = (0x2, "int") 193 | HAIR = (0x4, "int") 194 | 195 | PET = (0x8, "long") 196 | PET2 = (0x80000, "long") 197 | PET3 = (0x100000, "long") 198 | 199 | LEVEL = (0x10, "byte") 200 | JOB = (0x20, "short") 201 | STR = (0x40, "short") 202 | DEX = (0x80, "short") 203 | INT = (0x100, "short") 204 | LUK = (0x200, "short") 205 | 206 | HP = (0x400, "int") 207 | MAX_HP = (0x800, "int") 208 | MP = (0x1000, "int") 209 | MAX_MP = (0x2000, "int") 210 | 211 | AP = (0x4000, "short") 212 | SP = (0x8000, "short") 213 | 214 | EXP = (0x10000, "int") 215 | POP = (0x20000, "short") 216 | 217 | MONEY = (0x40000, "int") 218 | # TEMP_EXP = 0x200000 219 | -------------------------------------------------------------------------------- /mapy/game/inventory.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ..abstract import Inventory as _Inventory 4 | from ..constants import InventoryType 5 | from .item import * 6 | 7 | 8 | class InventoryManager: 9 | def __init__(self, character): 10 | self._character = character 11 | self.tracker = Tracker() 12 | self.inventories: dict[int, Inventory] = {} 13 | 14 | for i in range(1, 6): 15 | self.inventories[i] = Inventory(InventoryType(i), 28) 16 | 17 | def __iter__(self): 18 | return ((inv_type, inv.items) for inv_type, inv in self.inventories.items()) 19 | 20 | @property 21 | def updates(self): 22 | return self.tracker.inventory_changes 23 | 24 | def get(self, inventory_type: int | InventoryType) -> "Inventory": 25 | 26 | if isinstance(inventory_type, InventoryType): 27 | return self.inventories[inventory_type.value] 28 | 29 | elif isinstance(inventory_type, int): 30 | return self.inventories[inventory_type] 31 | 32 | else: 33 | raise ValueError( 34 | "Parameter 'inventory_type' must be of type InventoryType or int" 35 | ) 36 | 37 | def add(self, item: ItemSlotBase, slot=0): 38 | item_type = int(item.item_id / 1000000) 39 | inventory: Inventory = self.inventories[item_type] 40 | 41 | item_ = inventory.add(item, slot) 42 | 43 | if not item_: 44 | return 45 | 46 | slot = item_[0] 47 | item_ = item_[1] 48 | 49 | self.tracker.insert(slot, item_) 50 | 51 | 52 | class Tracker: 53 | def __init__(self): 54 | self.type = InventoryType.TRACKER 55 | self._starting = [] 56 | 57 | # Only update this on stat improvement, 58 | # movement, or new item added 59 | self._items = {i: {} for i in range(1, 6)} 60 | 61 | def insert(self, item, slot): 62 | self._items[int(item.item_id / 1000000)][slot] = item 63 | 64 | @property 65 | def inventory_changes(self): 66 | return [ 67 | {**item.__dict__, "inventory_type": inv_type, "position": slot} 68 | for inv_type, inventory in self._items.items() 69 | for slot, item in inventory.items() 70 | ] 71 | 72 | # def get_update(self): 73 | # return [{**item.__dict__, 74 | # 'inventory_type': inv_type, 75 | # 'position': slot 76 | # } for inv_type, inventory in self._items.items() 77 | # for slot, item in inventory.items()] 78 | 79 | def get_throwaway(self): 80 | throwaway = [] 81 | 82 | for _, inv in self._items.items(): 83 | for _, item in inv.items(): 84 | if item is None or item.inventory_item_id in self._starting: 85 | continue 86 | 87 | throwaway.append(item.inventory_item_id) 88 | 89 | return throwaway 90 | 91 | def copy(self, *inventories): 92 | for _, inventory in inventories: 93 | for _, item in inventory.items(): 94 | if item: 95 | self._starting.append(item.inventory_item_id) 96 | 97 | 98 | class Inventory(_Inventory): 99 | def __init__(self, type_, slots): 100 | self._unique_id = None 101 | self.type = type_ 102 | self.items: dict[int, Any] = {i: None for i in range(1, slots + 1)} 103 | self._slots = slots 104 | 105 | def __getitem__(self, key): 106 | return self.items.get(key) 107 | 108 | def __iter__(self): 109 | return (item for item in self.items) 110 | 111 | def get_free_slot(self): 112 | for i in range(1, self._slots + 1): 113 | if not self.items[i]: 114 | return i 115 | 116 | return None 117 | 118 | def add(self, item: ItemSlotBase, slot=None) -> tuple[int, ItemSlotBase | None]: 119 | items = None 120 | 121 | if isinstance(item, ItemSlotEquip): 122 | free_slot = self.get_free_slot() if not slot else slot 123 | 124 | if free_slot: 125 | self.items[free_slot] = item 126 | items = (free_slot, item) 127 | 128 | elif isinstance(item, ItemSlotBundle): 129 | # Get Slot with same item_id and not max bundle 130 | # or insert into free slot 131 | pass 132 | 133 | if not items: 134 | return (0, None) 135 | 136 | return items 137 | 138 | @property 139 | def slots(self): 140 | return self._slots 141 | 142 | def encode(self, packet): 143 | for slot, item in self.items.items(): 144 | if not item: 145 | continue 146 | 147 | packet.encode_byte(slot) 148 | item.encode(packet) 149 | 150 | packet.encode_byte(0) 151 | -------------------------------------------------------------------------------- /mapy/game/item.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from uuid import UUID, uuid4 3 | 4 | from attrs import define 5 | 6 | 7 | class ItemInventoryTypes(Enum): 8 | ItemSlotEquip = 0x1 9 | 10 | 11 | @define 12 | class ItemSlotBase(object): 13 | """Base item class for all items 14 | 15 | Parameters 16 | ---------- 17 | item_id: int 18 | Item temaplte ID 19 | cisn: int 20 | Cash Inventory Serial Numer 21 | Used for tracking cash items 22 | expire: :class:`datetime.datetime` 23 | Expiry date of the item, if any 24 | inventory_item_id: int 25 | Primary key to store the item in database 26 | flag: bool 27 | Determines whether item has been deleted, 28 | transfered, or stayed in inventory 29 | 30 | """ 31 | 32 | item_id: int = 0 33 | item_uuid: UUID = uuid4() 34 | cash_serial: int = 0 35 | source_type: int = 0 36 | expire: int = 0 37 | quantity: int = 0 38 | flag: int = 0 39 | 40 | def encode(self, packet) -> None: 41 | """Encode base item information onto packet 42 | 43 | Parameters 44 | ---------- 45 | packet: :class:`net.packets.Packet` 46 | The packet to encode the data onto 47 | 48 | """ 49 | 50 | packet.encode_int(self.item_id) 51 | packet.encode_byte(self.cash_serial == 0) 52 | 53 | if self.cash_serial: 54 | packet.encode_long(self.cash_serial) 55 | 56 | packet.encode_long(0) 57 | 58 | 59 | @define 60 | class ItemSlotEquip(ItemSlotBase): 61 | req_job: list[int] | None = list() 62 | ruc: int = 0 63 | cuc: int = 0 64 | 65 | _str: int = 0 66 | dex: int = 0 67 | _int: int = 0 68 | luk: int = 0 69 | hp: int = 0 70 | mp: int = 0 71 | weapon_attack: int = 0 72 | weapon_defense: int = 0 73 | magic_attack: int = 0 74 | magic_defense: int = 0 75 | accuracy: int = 0 76 | avoid: int = 0 77 | 78 | hands: int = 0 79 | speed: int = 0 80 | jump: int = 0 81 | 82 | title: str = "" 83 | craft: int = 0 84 | attribute: int = 0 85 | level_up_type: int = 0 86 | level: int = 0 87 | durability: int = 0 88 | iuc: int = 0 89 | exp: int = 0 90 | 91 | grade: int = 0 92 | chuc: int = 0 93 | 94 | option_1: int = 0 95 | option_2: int = 0 96 | option_3: int = 0 97 | socket_1: int = 0 98 | socket_2: int = 0 99 | 100 | lisn: int = 0 101 | storage_id: int = 0 102 | sn: int = 0 103 | 104 | def encode(self, packet): 105 | packet.encode_byte(1) 106 | 107 | super().encode(packet) 108 | 109 | packet.encode_byte(self.ruc) 110 | packet.encode_byte(self.cuc) 111 | packet.encode_short(self._str) 112 | packet.encode_short(self.dex) 113 | packet.encode_short(self._int) 114 | packet.encode_short(self.luk) 115 | packet.encode_short(self.hp) 116 | packet.encode_short(self.mp) 117 | packet.encode_short(self.weapon_attack) 118 | packet.encode_short(self.magic_attack) 119 | packet.encode_short(self.weapon_defense) 120 | packet.encode_short(self.magic_defense) 121 | packet.encode_short(self.accuracy) 122 | packet.encode_short(self.avoid) 123 | packet.encode_short(self.craft) 124 | packet.encode_short(self.speed) 125 | packet.encode_short(self.jump) 126 | packet.encode_string(self.title) 127 | packet.encode_short(self.attribute) 128 | 129 | packet.encode_byte(self.level_up_type) 130 | packet.encode_byte(self.level) 131 | packet.encode_int(self.exp) 132 | packet.encode_int(-1 & 0xFFFFFF) 133 | 134 | packet.encode_int(self.iuc) 135 | 136 | packet.encode_byte(self.grade) 137 | packet.encode_byte(self.chuc) 138 | 139 | packet.encode_short(self.option_1) 140 | packet.encode_short(self.option_2) 141 | packet.encode_short(self.option_3) 142 | packet.encode_short(self.socket_1) 143 | packet.encode_short(self.socket_2) 144 | 145 | if not self.cash_serial: 146 | packet.encode_long(0) 147 | 148 | packet.encode_long(0) 149 | packet.encode_int(0) 150 | 151 | 152 | @define 153 | class ItemSlotBundle(ItemSlotBase): 154 | number: int = 1 155 | attribute: int = 0 156 | lisn: int = 0 157 | title: str = "" 158 | 159 | def encode(self, packet): 160 | packet.encode_byte(2) 161 | 162 | super().encode(packet) 163 | 164 | packet.encode_short(self.number) 165 | packet.encode_string(self.title) 166 | packet.encode_short(self.attribute) 167 | 168 | if self.item_id / 10000 == 207: 169 | packet.encode_long(self.lisn) 170 | -------------------------------------------------------------------------------- /mapy/packet.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from io import BytesIO 3 | from struct import pack, unpack 4 | from typing import Any, Callable, Coroutine, ParamSpec, TypeVar 5 | 6 | from .opcodes import CRecvOps, CSendOps, OpCode 7 | from .tools import to_string 8 | 9 | WvsCenter = TypeVar("WvsCenter") 10 | P_ = ParamSpec("P_") 11 | 12 | # Junk codes for colorizing incoming packets from custom client 13 | debug_codes = [ 14 | ("r", ("|", "|")), 15 | ("lr", ("|", "&")), 16 | ("c", ("~", "~")), 17 | ("lc", ("~", "&")), 18 | ("y", ("#", "#")), 19 | ("ly", ("#", "&")), 20 | ("g", ("^", "^")), 21 | ("lg", ("^", "&")), 22 | ("m", ("@", "@")), 23 | ("lm", ("@", "&")), 24 | ] 25 | 26 | 27 | class DebugType(Enum): 28 | _byte = 0x1 29 | _short = 0x2 30 | _int = 0x4 31 | _long = 0x8 32 | _string = 0x10 33 | 34 | 35 | class ByteBuffer(BytesIO): 36 | """Base class for packet write and read operations""" 37 | 38 | def encode(self, _bytes): 39 | self.write(_bytes) 40 | return self 41 | 42 | def encode_byte(self, value): 43 | if isinstance(value, Enum): 44 | value = value.value 45 | 46 | self.write(bytes([value])) 47 | return self 48 | 49 | def encode_short(self, value): 50 | self.write(pack("H", value)) 51 | return self 52 | 53 | def encode_int(self, value): 54 | self.write(pack("I", value)) 55 | return self 56 | 57 | def encode_long(self, value): 58 | self.write(pack("Q", value)) 59 | return self 60 | 61 | def encode_buffer(self, buffer): 62 | self.write(buffer) 63 | return self 64 | 65 | def skip(self, count): 66 | self.write(bytes(count)) 67 | return self 68 | 69 | def encode_string(self, string): 70 | self.write(pack("H", len(string))) 71 | 72 | for ch in string: 73 | self.write(ch.encode()) 74 | 75 | return self 76 | 77 | def encode_fixed_string(self, string: str, length=13): 78 | self.write(pack(f"{length+1}s", string)) 79 | 80 | return self 81 | 82 | def encode_hex_string(self, string: str): 83 | string = string.strip(" -") 84 | self.write(bytes.fromhex(string)) 85 | return self 86 | 87 | def decode_byte(self): 88 | return self.read(1)[0] 89 | 90 | def decode_bool(self): 91 | return bool(self.decode_byte()) 92 | 93 | def decode_short(self): 94 | return unpack("H", self.read(2))[0] 95 | 96 | def decode_int(self): 97 | return unpack("I", self.read(4))[0] 98 | 99 | def decode_long(self): 100 | return unpack("Q", self.read(8))[0] 101 | 102 | def decode_buffer(self, size): 103 | return self.read(size) 104 | 105 | def decode_string(self): 106 | length = self.decode_short() 107 | string = "" 108 | 109 | for _ in range(length): 110 | string += self.read(1).decode() 111 | 112 | return string 113 | 114 | 115 | class Packet(ByteBuffer): 116 | """Packet class use in all send / recv opertions 117 | 118 | Parameters 119 | ---------- 120 | data: bytes 121 | The initial data to load into the packet 122 | op_code: :class:`OpCodes` 123 | OpCode used to encode the first short onto the packet 124 | op_codes: :class:`OpCodes` 125 | Which enum to try to get the op_code from 126 | 127 | """ 128 | 129 | def __init__(self, data: bytes | None = None, op_code=None, raw=False): 130 | self.op_code = OpCode 131 | 132 | if data: 133 | super().__init__(data) 134 | 135 | else: 136 | super().__init__() 137 | 138 | if isinstance(op_code, type(None)): 139 | return 140 | 141 | self.op_code = op_code 142 | 143 | if isinstance(self.op_code, int): 144 | self.encode_short(self.op_code) 145 | elif isinstance(self.op_code, (CSendOps, CRecvOps)): 146 | self.encode_short(self.op_code.value) 147 | 148 | if not raw and data: 149 | self.op_code = CRecvOps(self.decode_short()) 150 | 151 | @property 152 | def name(self): 153 | if isinstance(self.op_code, int): 154 | return self.op_code 155 | 156 | elif isinstance(self.op_code, (CSendOps, CRecvOps)): 157 | return self.op_code.name 158 | 159 | def to_array(self): 160 | return self.getvalue() 161 | 162 | def to_string(self): 163 | return to_string(self.getvalue()) 164 | 165 | def __len__(self): 166 | return len(self.getvalue()) 167 | 168 | 169 | class PacketHandler: 170 | def __init__(self, name, callback, **kwargs): 171 | self.name = name 172 | self.callback = callback 173 | self.op_code = kwargs.get("op_code") 174 | 175 | 176 | def packet_handler(op_code=None) -> Any: 177 | def wrap(func: Callable[P_, Coroutine]): 178 | 179 | return PacketHandler(func.__name__, func, op_code=op_code) 180 | 181 | return wrap 182 | -------------------------------------------------------------------------------- /mapy/abstract.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta 2 | 3 | 4 | class Serializable(metaclass=ABCMeta): 5 | def __serialize__(self): 6 | serialized = {} 7 | for key, value in self.__dict__.items(): 8 | if issubclass(value.__class__, Serializable): 9 | value = value.__serialize__() 10 | 11 | serialized[key] = value 12 | 13 | return serialized 14 | 15 | 16 | class WildcardData: 17 | def __new__(cls, *args, **kwargs): 18 | old_init = cls.__init__ 19 | 20 | def _new_init_(self, *_args, **_kwargs): 21 | cleaned = {} 22 | for key, value in _kwargs.items(): 23 | if key not in dir(cls): 24 | continue 25 | cleaned[key] = value 26 | old_init(self, *_args, **cleaned) 27 | 28 | cls.__init__ = _new_init_ 29 | return super(WildcardData, cls).__new__(cls) 30 | 31 | 32 | class Inventory(metaclass=ABCMeta): 33 | def __init__(self): 34 | self.items = {} 35 | 36 | def add(self, item, slot=None): 37 | return NotImplemented 38 | 39 | 40 | # class BITN(int): 41 | # ... 42 | 43 | 44 | # class BIT1(int): 45 | # ... 46 | 47 | 48 | # BIT = TypeVar("BIT", BIT1, BITN) 49 | 50 | 51 | # class FlagType(Generic[BIT]): 52 | # _name_ = "" 53 | # __dict__ = { 54 | # "_member_map_": {}, 55 | # "_member_names_": [], 56 | # "_name_": "", 57 | # "_value_": 0, 58 | # "__members__": {}, 59 | # "__name__": "", 60 | # "_members_": [], 61 | # } 62 | 63 | # def __str__(self): 64 | # return self._name_ 65 | 66 | 67 | # class BitMask(FlagType, Flag): 68 | # """Subclassable Enum, linters throw a fit not really sure what to do about that""" 69 | 70 | # __original_call__ = EnumMeta.__call__ 71 | # setattr(EnumMeta, "__original_call__", __original_call__) 72 | # __orig_getattr__ = getattr(EnumMeta, "__getattr__") 73 | # setattr(EnumMeta, "__orig_getattr__", getattr(EnumMeta, "__getattr__")) 74 | 75 | # def __new__(cls, *args): 76 | # if args[0] != 0 and "NONE" not in cls._member_map_.keys(): 77 | # if "NONE" not in cls._member_map_.keys(): 78 | # none = cls(0) 79 | # none._name_ = "NONE" 80 | # none._value_ = 0 81 | # cls.NONE = none 82 | # cls._member_map_["NONE"] = none 83 | # cls._value2member_map_ |= {0: none} 84 | # cls._member_names_.append("NONE") 85 | 86 | # return super(BitMask).__init__(args[0]) 87 | 88 | # def __getattr__(self, name): 89 | # if ( 90 | # isinstance(self, type) 91 | # and name not in BitMask.__orig_getattr__(self, "_member_names_") 92 | # and (issubclass(self, BitMask) or issubclass(self, IntFlag)) 93 | # ): 94 | # new_val = len(self._member_names_) 95 | # new = self(new_val) 96 | # new._name_ = name 97 | # # setattr(cls, name, new_val) 98 | # self._member_map_[name] = new 99 | # self._value2member_map_[new_val] = new 100 | # self._members_.append(new) 101 | # self._member_names_.append(name) 102 | # return new 103 | 104 | # return getattr(EnumMeta, "__orig_getattr__")(self, name) 105 | 106 | # def __call__( 107 | # self, value, names=None, *, module=None, qualname=None, _type=None, start=1 108 | # ): 109 | # _names = names 110 | # if isinstance(self, type) and not issubclass(self, BitMask): 111 | # return self.__class__.__original_call__( 112 | # self, # type: ignore 113 | # value, 114 | # _names, # type: ignore 115 | # module=module, 116 | # qualname=qualname, 117 | # type=_type, 118 | # start=start, 119 | # ) 120 | 121 | # if isinstance(self, type) and ( 122 | # (issubclass(self, BitMask) and names is not None) 123 | # or not issubclass(self, BitMask) 124 | # ): 125 | # return getattr(EnumMeta, "__original_call__")(self, value, names=_names) 126 | 127 | # return self.__new__(self, value) 128 | 129 | # setattr(EnumMeta, "__call__", __call__) 130 | # setattr(EnumMeta, "__getattr__", __getattr__) 131 | 132 | # def __str__(self): 133 | # print(self) 134 | # return self._name_ 135 | 136 | # def __repr__(self): 137 | # print(self) 138 | # return self._name_ 139 | 140 | 141 | class ObjectPool: 142 | def __init__(self, field): 143 | self.field = field 144 | self.cache = {} 145 | self.uid_base = 1000 146 | 147 | @property 148 | def new_uid(self): 149 | self.uid_base += 1 150 | return self.uid_base 151 | 152 | def add(self, value): 153 | value.obj_id = self.new_uid 154 | self.cache[value.obj_id] = value 155 | 156 | def remove(self, key): 157 | return self.cache.pop(key) 158 | 159 | def clear(self): 160 | self.cache = {} 161 | 162 | def get(self, key): 163 | return self.cache.get(key, None) 164 | 165 | def __enumerator__(self): 166 | return (obj for obj in self.cache.values()) 167 | 168 | def __iter__(self): 169 | return (obj for obj in self.cache.values()) 170 | 171 | def __aiter__(self): 172 | return self.__iter__() 173 | -------------------------------------------------------------------------------- /mapy/scripting.py: -------------------------------------------------------------------------------- 1 | from asyncio import Queue, get_running_loop 2 | from dataclasses import dataclass 3 | from io import TextIOBase 4 | from pathlib import Path 5 | 6 | from aiofiles import open as async_open 7 | 8 | from .opcodes import CSendOps 9 | from .packet import Packet 10 | 11 | 12 | class ScriptBase: 13 | __script_cache__ = {} 14 | 15 | def __init__(self, script, client): 16 | self._file_contents = TextIOBase() 17 | self._script = None 18 | self._parent = client 19 | self._context = None 20 | self._read_task = get_running_loop().create_task(self.read_file(script)) 21 | 22 | async def read_file(self, path): 23 | try: 24 | if self.__script_cache__.get(path.name, None): 25 | self._file_contents = self.__script_cache__[path.name] 26 | else: 27 | async with async_open(path, "r") as f: 28 | self._file_contents.write(await f.read()) 29 | self._file_contents.seek(0) 30 | self.__script_cache__[path.name] = self._file_contents 31 | 32 | self._script = compile( 33 | "async def ex():\n" 34 | + "".join([f" {line}" for line in self._file_contents.readlines()]), 35 | "", 36 | "exec", 37 | ) 38 | self._file_contents.seek(0) 39 | 40 | finally: 41 | return True 42 | 43 | async def execute(self): 44 | async def run(script, _globals): 45 | exec(script, _globals) 46 | await _globals["ex"]() 47 | 48 | env = {"ctx": self._context} 49 | env.update(globals()) 50 | await run(self._script, env) 51 | 52 | self._parent.npc_script = None 53 | 54 | @property 55 | def parent(self): 56 | return self._parent 57 | 58 | 59 | class NpcScript(ScriptBase): 60 | def __init__(self, client, /, npc_id=None, default=False, file=None): 61 | script = Path() 62 | if not file: 63 | script = Path( 64 | f"scripts/npc/{'default' if default or not npc_id else npc_id}.py" 65 | ) 66 | else: 67 | if isinstance(file, Path): 68 | script = file 69 | 70 | if not script.exists(): 71 | return 72 | 73 | super().__init__(script, client) 74 | self._npc_id = npc_id 75 | self._context = NpcContext(self) 76 | self._last_msg_type = None 77 | 78 | self._prev_msgs = [] 79 | self._prev_id = 0 80 | self._response = Queue(maxsize=1) 81 | 82 | @property 83 | def npc_id(self): 84 | return self._npc_id 85 | 86 | @property 87 | def last_msg_type(self): 88 | return self._last_msg_type 89 | 90 | async def send_message(self, type_, action, flag=4, param=0): 91 | await self.send_dialogue(type_, action, flag, param) 92 | 93 | resp = await self._response.get() 94 | return resp 95 | 96 | async def send_dialogue(self, type_, action, flag, param): 97 | packet = Packet(op_code=CSendOps.LP_ScriptMessage) 98 | packet.encode_byte(flag) 99 | packet.encode_int(self._npc_id) 100 | packet.encode_byte(type_) 101 | packet.encode_byte(param) 102 | 103 | action(packet) 104 | 105 | self._last_msg_type = type_ 106 | self._response.task_done() 107 | await self._parent.send_packet(packet) 108 | 109 | async def reuse_dialogue(self, msg): 110 | await self.send_dialogue(0, msg.encode, 4, 0) 111 | 112 | async def proceed_back(self): 113 | if self._prev_id == 0: 114 | return 115 | 116 | self._prev_id -= 1 117 | 118 | await self.reuse_dialogue(self._prev_msgs[self._prev_id]) 119 | 120 | async def proceed_next(self, resp): 121 | self._prev_id += 1 122 | 123 | if self._prev_id < len(self._prev_msgs): 124 | await self.reuse_dialogue(self._prev_msgs[self._prev_id]) 125 | 126 | else: 127 | self._response.task_done() 128 | await self._response.put(resp) 129 | 130 | def end_chat(self): 131 | self.parent.npc_script = None 132 | self._response.task_done() 133 | 134 | @staticmethod 135 | def get_script(npc_id, client): 136 | if (p := Path(f"scripts/npc/{npc_id}.py")).exists() and p.is_file(): 137 | return NpcScript(client, file=p) 138 | return NpcScript(client, default=True) 139 | 140 | 141 | class ContextBase: 142 | def __init__(self, script): 143 | self._script = script 144 | 145 | 146 | @dataclass 147 | class Message: 148 | msg: str = "" 149 | prev: bool = False 150 | nxt: bool = False 151 | 152 | def encode(self, packet): 153 | packet.encode_string(self.msg) 154 | packet.encode_byte(self.prev) 155 | packet.encode_byte(self.nxt) 156 | 157 | 158 | class NpcContext(ContextBase): 159 | @property 160 | def npc_id(self): 161 | return self._script.npc_id 162 | 163 | async def say(self, msg, prev=False, nxt=False): 164 | self._script._prev_msgs.append(Message(msg, prev, nxt)) 165 | 166 | def action(packet): 167 | packet.encode_string(msg) 168 | packet.encode_byte(prev) 169 | packet.encode_byte(nxt) 170 | 171 | await self._script.send_message(0, action) 172 | 173 | async def ask_yes_no(self, msg): 174 | def action(packet): 175 | packet.encode_string(msg) 176 | 177 | await self._script.send_message(2, action) 178 | 179 | def end_chat(self): 180 | self._script.end_chat() 181 | -------------------------------------------------------------------------------- /mapy/client.py: -------------------------------------------------------------------------------- 1 | from asyncio import Lock, get_running_loop 2 | from random import randint 3 | from typing import Any 4 | 5 | from .constants import Config, Worlds 6 | from .crypto import MapleAes, MapleIV, decrypt_transform, encrypt_transform 7 | from .logger import Logger 8 | from .packet import Packet 9 | 10 | RECV_SIZE = 4096 11 | 12 | 13 | class ClientBase: 14 | __slots__ = ( 15 | "_socket", 16 | "_port", 17 | "_overflow_buff", 18 | "_recv_buff", 19 | "_logged_in", 20 | "_world_id", 21 | "_channel_id", 22 | "_lock", 23 | "_m_riv", 24 | "_m_siv", 25 | "_parent", 26 | "_logger", 27 | "_center", 28 | ) 29 | 30 | def __init__(self, parent, socket): 31 | self._socket = socket 32 | self._parent = self._center = parent 33 | 34 | self._port = None 35 | self._overflow_buff = bytearray(1024) 36 | self._recv_buff = bytearray(1024) 37 | self._logged_in = False 38 | self._world_id = 0 39 | self._lock = Lock() 40 | self._m_riv = None 41 | self._m_siv = None 42 | self._logger = Logger(f"Client ({self.ip})") 43 | 44 | @property 45 | def connected_channel(self): 46 | return self._center.worlds[self._world_id].channels[self._channel_id] 47 | 48 | @property 49 | def ip(self): 50 | return self._socket.getpeername()[0] 51 | 52 | @property 53 | def data(self): 54 | return self._parent.data 55 | 56 | @property 57 | def identifier(self): 58 | return self._socket.getpeername() 59 | 60 | def dispatch(self, packet): 61 | self._parent.push(self, packet) 62 | 63 | def close(self): 64 | return self._socket.close() 65 | 66 | async def initialize(self): 67 | self._m_siv = MapleIV(randint(0, 1 << 31)) 68 | self._m_riv = MapleIV(randint(0, 1 << 31)) 69 | 70 | packet = Packet(op_code=0x0E) 71 | packet.encode_short(Config.VERSION) 72 | packet.encode_string(Config.SUB_VERSION) 73 | packet.encode_int(self._m_riv.value) 74 | packet.encode_int(self._m_siv.value) 75 | packet.encode_byte(Config.LOCALE) 76 | 77 | await self.send_packet_raw(packet) 78 | 79 | while get_running_loop().is_running(): 80 | 81 | if not self._overflow_buff: 82 | await get_running_loop().sock_recv_into( 83 | self._socket, memoryview(self._recv_buff)[:] 84 | ) 85 | 86 | if not self._recv_buff: 87 | await self._parent.on_client_disconnect(self) 88 | return 89 | 90 | else: 91 | self._recv_buff = self._overflow_buff 92 | self._overflow_buff = bytearray() 93 | 94 | if self._m_riv: 95 | async with self._lock: 96 | length = MapleAes.get_length(self._recv_buff) 97 | if length != len(self._recv_buff) - 4: 98 | self._overflow_buff = self._recv_buff[length + 4 :] 99 | self._recv_buff = self._recv_buff[: length + 4] 100 | 101 | self._recv_buff = self.manipulate_buffer(self._recv_buff) 102 | 103 | self.dispatch(Packet(data=self._recv_buff)) 104 | self._recv_buff = bytearray() 105 | 106 | async def send_packet(self, out_packet): 107 | self._logger.log_opkt(f"{out_packet.to_string()}") 108 | 109 | packet_length = len(out_packet) 110 | packet = bytearray(out_packet.getvalue()) 111 | 112 | buf = packet[:] 113 | 114 | final = bytearray(packet_length + 4) 115 | async with self._lock: 116 | MapleAes.get_header(final, self._m_siv, packet_length, Config.VERSION) 117 | buf = encrypt_transform(buf) 118 | final[4:] = MapleAes.transform(buf, self._m_siv) 119 | 120 | await get_running_loop().sock_sendall(self._socket, final) 121 | 122 | async def send_packet_raw(self, packet): 123 | self._logger.log_opkt(f"{packet.name} {self.ip} {packet.to_string()}") 124 | 125 | await get_running_loop().sock_sendall(self._socket, packet.getvalue()) 126 | 127 | def manipulate_buffer(self, buffer): 128 | buf = bytearray(buffer)[4:] 129 | 130 | buf = MapleAes.transform(buf, self._m_riv) 131 | buf = decrypt_transform(buf) 132 | 133 | return buf 134 | 135 | 136 | class WvsLoginClient(ClientBase): 137 | """LoginClient 138 | 139 | Parameters 140 | ---------- 141 | 142 | parent: :class:`ServerBase` 143 | Parent server client is connecting to 144 | socket: :class:`Socket` 145 | Socket holding client - server connection 146 | name: str 147 | Name identifying type of client 148 | """ 149 | 150 | def __init__(self, parent, socket): 151 | super().__init__(parent, socket) 152 | 153 | self._account = None 154 | self._avatars = [] 155 | 156 | async def login(self, username, password): 157 | resp, account = await ( 158 | self.data.account(username=username, password=password).login() 159 | ) 160 | 161 | if not resp: 162 | self._account = account 163 | self._logged_in = True 164 | return 0 165 | 166 | return resp 167 | 168 | async def load_avatars(self, world_id=None): 169 | if not self._account: 170 | self._avatars = [] 171 | return 172 | 173 | self._avatars = await ( 174 | self.data.account(id=self._account.id).get_entries(world_id=world_id) 175 | ) 176 | 177 | @property 178 | def account_id(self): 179 | return getattr(self._account, "id", -1) 180 | 181 | 182 | class WvsGameClient(ClientBase): 183 | def __init__(self, parent, socket): 184 | super().__init__(parent, socket) 185 | 186 | self._channel_id = parent.channel_id 187 | self._world_id = 0 188 | self._character: Any | None = None 189 | self._npc_script = None 190 | self._sent_char_data = False 191 | 192 | @property 193 | def world_id(self): 194 | if not self._logged_in: 195 | return -1 196 | 197 | return self._world_id or -1 198 | 199 | @world_id.setter 200 | def world_id(self, value): 201 | if isinstance(value, Worlds): 202 | self._world_id = value._value_ 203 | elif isinstance(value, int): 204 | self._world_id = value 205 | else: 206 | raise ValueError() 207 | 208 | async def broadcast(self, packet): 209 | if not self._character: 210 | return 211 | 212 | await self._character.field.broadcast(packet, self) 213 | 214 | def get_field(self): 215 | if not self._character: 216 | return 217 | 218 | return self.connected_channel.get_field(self._character.field_id) 219 | -------------------------------------------------------------------------------- /mapy/database/types.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import inspect 4 | import pydoc 5 | 6 | from .errors import SchemaError 7 | 8 | 9 | class SQLType: 10 | python = type(None) 11 | 12 | def to_dict(self): 13 | dct = self.__dict__.copy() 14 | clas = self.__class__ 15 | dct["__meta__"] = clas.__module__ + "." + clas.__qualname__ 16 | return dct 17 | 18 | @classmethod 19 | def from_dict(cls, data): 20 | meta = data.pop("__meta__") 21 | given = cls.__module__ + "." + cls.__qualname__ 22 | 23 | if given != meta: 24 | cls = pydoc.locate(meta) 25 | if cls is None: 26 | raise RuntimeError(f'Could not locate "{meta}".') 27 | 28 | self = cls.__new__(type(cls)) 29 | self.__dict__.update(data) 30 | return self 31 | 32 | def __eq__(self, other): 33 | return isinstance(other, 34 | self.__class__) and self.__dict__ == other.__dict__ 35 | 36 | def __ne__(self, other): 37 | return not self.__eq__(other) 38 | 39 | def to_sql(self): 40 | raise NotImplementedError() 41 | 42 | def is_real_type(self): 43 | return True 44 | 45 | 46 | class Boolean(SQLType): 47 | python = bool 48 | 49 | def to_sql(self): 50 | return "BOOLEAN" 51 | 52 | 53 | class Date(SQLType): 54 | python = datetime.date 55 | 56 | def to_sql(self): 57 | return "DATE" 58 | 59 | 60 | class Datetime(SQLType): 61 | python = datetime.datetime 62 | 63 | def __init__(self, *, timezone=False): 64 | self.timezone = timezone 65 | 66 | def to_sql(self): 67 | if self.timezone: 68 | return "TIMESTAMP WITH TIMEZONE" 69 | 70 | return "TIMESTAMP" 71 | 72 | 73 | class Double(SQLType): 74 | python = float 75 | 76 | def to_sql(self): 77 | return "REAL" 78 | 79 | 80 | class Integer(SQLType): 81 | python = int 82 | 83 | def __init__(self, *, big=False, small=False, auto_increment=False): 84 | self.big = big 85 | self.small = small 86 | self.auto_increment = auto_increment 87 | 88 | if big and small: 89 | raise SchemaError("Integer cannot be both big and small") 90 | 91 | def to_sql(self): 92 | if self.auto_increment: 93 | if self.big: 94 | return "BIGSERIAL" 95 | if self.small: 96 | return "SMALLSERIAL" 97 | return "SERIAL" 98 | 99 | if self.big: 100 | return "BIGINT" 101 | if self.small: 102 | return "SMALLINT" 103 | return "INTEGER" 104 | 105 | def is_real_type(self): 106 | return not self.auto_increment 107 | 108 | 109 | class Interval(SQLType): 110 | python = datetime.timedelta 111 | valid_fields = ( 112 | "YEAR", 113 | "MONTH", 114 | "DAY", 115 | "HOUR", 116 | "MINUTE", 117 | "SECOND", 118 | "YEAR TO MONTH", 119 | "DAY TO HOUR", 120 | "DAY TO MINUTE", 121 | "DAY TO SECOND", 122 | "HOUR TO MINUTE", 123 | "HOUR TO SECOND", 124 | "MINUTE TO SECOND", 125 | ) 126 | 127 | def __init__(self, field=None): 128 | if field: 129 | field = field.upper() 130 | if field not in self.valid_fields: 131 | raise SchemaError("invalid interval specified") 132 | 133 | self.field = field 134 | 135 | def to_sql(self): 136 | if self.field: 137 | return "INTERVAL " + self.field 138 | return "INTERVAL" 139 | 140 | 141 | class Decimal(SQLType): 142 | python = decimal.Decimal 143 | 144 | def __init__(self, *, precision=None, scale=None): 145 | if precision is not None: 146 | if precision < 0 or precision > 1000: 147 | raise SchemaError( 148 | "precision must be greater than 0 and below 1000") 149 | if scale is None: 150 | scale = 0 151 | 152 | self.precision = precision 153 | self.scale = scale 154 | 155 | def to_sql(self): 156 | if self.precision is not None: 157 | return f"NUMERIC({self.precision}, {self.scale})" 158 | return "NUMERIC" 159 | 160 | 161 | class Numeric(SQLType): 162 | python = decimal.Decimal 163 | 164 | def __init__(self, *, precision=None, scale=None): 165 | if precision is not None: 166 | if precision < 0 or precision > 1000: 167 | raise SchemaError("precision must be greater than 0" 168 | "and below 1000") 169 | if scale is None: 170 | scale = 0 171 | 172 | self.precision = precision 173 | self.scale = scale 174 | 175 | def to_sql(self): 176 | if self.precision is not None: 177 | return f"NUMERIC({self.precision}, {self.scale})" 178 | return "NUMERIC" 179 | 180 | 181 | class String(SQLType): 182 | python = str 183 | 184 | def __init__(self, *, length=None, fixed=False): 185 | self.length = length 186 | self.fixed = fixed 187 | 188 | if fixed and length is None: 189 | raise SchemaError("Cannot have fixed string with no length") 190 | 191 | def to_sql(self): 192 | if self.length is None: 193 | return "TEXT" 194 | if self.fixed: 195 | return f"CHAR({self.length})" 196 | return f"VARCHAR({self.length})" 197 | 198 | 199 | class Time(SQLType): 200 | python = datetime.time 201 | 202 | def __init__(self, *, timezone=False): 203 | self.timezone = timezone 204 | 205 | def to_sql(self): 206 | if self.timezone: 207 | return "TIME WITH TIME ZONE" 208 | return "TIME" 209 | 210 | 211 | class JSON(SQLType): 212 | python = None 213 | 214 | def to_sql(self): 215 | return "JSONB" 216 | 217 | 218 | class ForeignKey(SQLType): 219 | 220 | def __init__( 221 | self, 222 | table, 223 | column, 224 | *, 225 | sql_type=None, 226 | on_delete="CASCADE", 227 | on_update="NO ACTION", 228 | ): 229 | if not table or not isinstance(table, str): 230 | raise SchemaError("Missing table to reference (must be string)") 231 | 232 | valid_actions = ("NO ACTION", "RESTRICT", "CASCADE", "SET NULL", 233 | "SET DEFAULT") 234 | 235 | on_delete = on_delete.upper() 236 | on_update = on_update.upper() 237 | 238 | if on_delete not in valid_actions: 239 | raise TypeError("on_delete must be one of %s." % valid_actions) 240 | 241 | if on_update not in valid_actions: 242 | raise TypeError("on_update must be one of %s." % valid_actions) 243 | 244 | self.table = table 245 | self.column = column 246 | self.on_update = on_update 247 | self.on_delete = on_delete 248 | 249 | if sql_type is None: 250 | sql_type = Integer 251 | 252 | if inspect.isclass(sql_type): 253 | sql_type = sql_type() 254 | 255 | if not isinstance(sql_type, SQLType): 256 | raise TypeError("Cannot have non-SQLType derived sql_type") 257 | 258 | if not sql_type.is_real_type(): 259 | raise SchemaError('sql_type must be a "real" type') 260 | 261 | self.sql_type = sql_type.to_sql() 262 | 263 | def is_real_type(self): 264 | return False 265 | 266 | def to_sql(self): 267 | return (f"{self.column} REFERENCES {self.table} ({self.column})" 268 | f" ON DELETE {self.on_delete} ON UPDATE {self.on_update}") 269 | 270 | 271 | class ArraySQL(SQLType): 272 | 273 | def __init__(self, inner_type, size: int | None = None): 274 | if not isinstance(inner_type, SQLType): 275 | raise SchemaError("Array inner type must be an SQLType") 276 | self.type = inner_type 277 | self.size = size 278 | 279 | def to_sql(self): 280 | if self.size: 281 | return f"{self.type.to_sql()}[{self.size}]" 282 | return f"{self.type.to_sql()}[]" 283 | -------------------------------------------------------------------------------- /mapy/game/field.py: -------------------------------------------------------------------------------- 1 | from math import atan, cos 2 | from random import choice 3 | from typing import Type 4 | 5 | from attrs import define, field 6 | 7 | from ..abstract import ObjectPool 8 | from ..cpacket import CPacket 9 | from ..packet import ByteBuffer 10 | from ..tools import TagPoint 11 | 12 | 13 | class MovePath: 14 | def __init__(self, x=0, y=0, foothold=0, position=0): 15 | self.x = x 16 | self.y = y 17 | self.foothold = foothold 18 | self.stance = position 19 | self.vx = None 20 | self.vy = None 21 | 22 | def decode_move_path(self, move_path): 23 | ipacket = ByteBuffer(move_path) 24 | 25 | self.x = ipacket.decode_short() 26 | self.y = ipacket.decode_short() 27 | self.vx = ipacket.decode_short() 28 | self.vy = ipacket.decode_short() 29 | 30 | size = ipacket.decode_byte() 31 | 32 | for _ in range(size): 33 | cmd = ipacket.decode_byte() 34 | 35 | if cmd == 0: 36 | self.x = ipacket.decode_short() 37 | self.y = ipacket.decode_short() 38 | _ = ipacket.decode_short() # xwob 39 | _ = ipacket.decode_short() # ywob 40 | self.foothold = ipacket.decode_short() 41 | _ = ipacket.decode_short() # xoff 42 | _ = ipacket.decode_short() # yoff 43 | self.stance = ipacket.decode_byte() 44 | _ = ipacket.decode_short() # duration 45 | elif cmd == 1: 46 | _ = ipacket.decode_short() # xmod 47 | _ = ipacket.decode_short() # ymod 48 | self.stance = ipacket.decode_short() 49 | _ = ipacket.decode_short() # duration 50 | elif cmd == 27: 51 | self.stance = ipacket.decode_byte() 52 | _ = ipacket.decode_short() # unk 53 | else: 54 | break 55 | 56 | def __str__(self): 57 | return ( 58 | f"Position: {self.x},{self.y} - Foothold: {self.foothold} " 59 | f"- Stance: {self.stance}" 60 | ) 61 | 62 | 63 | @define 64 | class FieldObject(object): 65 | _obj_id: int = -1 66 | _position: MovePath = MovePath() 67 | _field: Type["Field"] | None = None 68 | 69 | 70 | @define 71 | class Foothold(object): 72 | id: int = 0 73 | prev: int = 0 74 | next: int = 0 75 | x1: int = 0 76 | y1: int = 0 77 | x2: int = 0 78 | y2: int = 0 79 | 80 | @property 81 | def wall(self): 82 | return self.x1 == self.x2 83 | 84 | def compare_to(self, foothold): 85 | if self.y2 < foothold.y1: 86 | return -1 87 | if self.y1 > foothold.y2: 88 | return 1 89 | return 0 90 | 91 | 92 | @define 93 | class Portal(object): 94 | _id: int = field(default=0) 95 | name: str = "" 96 | _type: int = field(default=0) 97 | destination: int = 0 98 | destination_label: str = "" 99 | x: int = 0 100 | y: int = 0 101 | 102 | def __init__( 103 | self, 104 | id: int = 0, 105 | name: str = "", 106 | type_: int = 0, 107 | destination: int = 0, 108 | destination_label: str = "", 109 | x: int = 0, 110 | y: int = 0, 111 | ): 112 | self.id = id 113 | self.name = name or "" 114 | self.type = type_ 115 | self.destination = destination 116 | self.destination_label = destination_label or "" 117 | self.x = x 118 | self.y = y 119 | self.point = TagPoint(self.x, self.y) 120 | 121 | def __str__(self): 122 | return f"{id} @ {self.point} -> {self.destination}" 123 | 124 | 125 | @define 126 | class Life(FieldObject): 127 | def __init__( 128 | self, 129 | life_id: int = 0, 130 | life_type: str = "", 131 | foothold: int = 0, 132 | x: int = 0, 133 | y: int = 0, 134 | cy: int = 0, 135 | f: int = 0, 136 | hide: int = 0, 137 | rx0: int = 0, 138 | rx1: int = 0, 139 | mob_time: int = 0, 140 | **_, 141 | ): 142 | self.life_id: int = life_id 143 | self.life_type: str = life_type or "" 144 | self.foothold: int = foothold 145 | self.x: int = x 146 | self.y: int = y 147 | self.cy: int = cy 148 | self.f: int = f 149 | self.hide: int = hide 150 | self.rx0: int = rx0 # min click position 151 | self.rx1: int = rx1 # max click position 152 | self.mob_time: int = 0 153 | 154 | 155 | @define 156 | class Mob(Life): 157 | mob_id: int = 0 158 | hp: int = 0 159 | mp: int = 0 160 | hp_recovery: int = 0 161 | mp_recovery: int = 0 162 | exp: int = 0 163 | physical_attack: int = 0 164 | 165 | def __init__( 166 | self, 167 | mob_id: int = 0, 168 | hp: int = 0, 169 | mp: int = 0, 170 | hp_recovery: int = 0, 171 | mp_recovery: int = 0, 172 | exp: int = 0, 173 | physcial_attack: int = 0, 174 | **data, 175 | ): 176 | super().__init__(**data) 177 | self.mob_id = mob_id 178 | self.hp = hp 179 | self.hp_recovery = hp_recovery 180 | self.mp = mp 181 | self.mp_recovery = mp_recovery 182 | self.exp = exp 183 | self.physical_attack = physcial_attack 184 | self.attackers = {} 185 | self.pos = MovePath(self.x, self.cy, self.foothold) 186 | self.cur_hp = self.hp 187 | self.cur_mp = self.mp 188 | self.controller = None 189 | 190 | @property 191 | def dead(self): 192 | return self.cur_hp <= 0 193 | 194 | def damage(self, character, amount): 195 | pass 196 | 197 | def encode_init(self, packet): 198 | packet.encode_int(self._obj_id) 199 | packet.encode_byte(5) 200 | packet.encode_int(self.life_id) 201 | 202 | # Set Temporary Stat 203 | packet.encode_long(0) 204 | packet.encode_long(0) 205 | 206 | packet.encode_short(self.pos.x) 207 | packet.encode_short(self.pos.y) 208 | packet.encode_byte(0 & 1 | 2 * 2) 209 | packet.encode_short(self.pos.foothold) 210 | packet.encode_short(self.pos.foothold) 211 | 212 | packet.encode_byte(abs(-2)) 213 | 214 | packet.encode_byte(0) 215 | packet.encode_int(0) 216 | packet.encode_int(0) 217 | 218 | 219 | @define 220 | class Npc(Life): 221 | def __init__(self, **data): 222 | super().__init__(**data) 223 | self.id = self.life_id 224 | self.pos = MovePath(self.x, self.cy, self.foothold) 225 | 226 | 227 | class Field: 228 | def __init__(self, map_id): 229 | self.map_id = map_id 230 | # self.characters = [] 231 | # self.sockets = {} 232 | 233 | self.portals = PortalManager() 234 | self.footholds = FootholdManager() 235 | self.clients = UserPool(self) 236 | self.mobs = MobPool(map_id) 237 | self.npcs = NpcPool(map_id) 238 | 239 | @property 240 | def id(self): 241 | return self.map_id 242 | 243 | async def add(self, client): 244 | character = client.character 245 | 246 | if client.sent_char_data: 247 | await client.send_packet( 248 | CPacket.set_field(character, False, client.channel_id) 249 | ) 250 | 251 | else: 252 | client.sent_char_data = True 253 | character.stats.portal = 0 254 | 255 | await client.send_packet( 256 | CPacket.set_field(character, True, client.channel_id) 257 | ) 258 | 259 | for _character in self.clients.characters: 260 | await client.send_packet(CPacket.user_enter_field(_character)) 261 | 262 | await self.spawn_mobs(client) 263 | await self.spawn_npcs(client) 264 | 265 | self.clients.add(client) 266 | 267 | await self.broadcast(CPacket.user_enter_field(character)) 268 | 269 | async def broadcast(self, packet, *ignore): 270 | for client in self.clients: 271 | if client in ignore or client.character in ignore: 272 | continue 273 | 274 | await client.send_packet(packet) 275 | 276 | async def swap_mob_controller(self, client, mob): 277 | if mob.controller: 278 | controller = next( 279 | filter( 280 | lambda c: c.character.id == mob.controller, () 281 | ), # FIXME: IDRCC WHERE WE'RE GRABBIN THIS SHIT ITS SOMEWHERE IN THERE 282 | None, 283 | ) 284 | if controller: 285 | await controller.send_packet(CPacket.mob_change_controller(mob, 0)) 286 | 287 | mob.controller = client.character.id 288 | await client.send_packet(CPacket.mob_change_controller(mob, 1)) 289 | 290 | async def spawn_mobs(self, client): 291 | for mob in self.mobs: 292 | if mob.controller == 0: 293 | await self.swap_mob_controller(client, mob) 294 | 295 | await client.send_packet(CPacket.mob_enter_field(mob)) 296 | 297 | async def spawn_npcs(self, client): 298 | for npc in self.npcs: 299 | await client.send_packet(CPacket.npc_enter_field(npc)) 300 | 301 | 302 | class MobPool(ObjectPool): 303 | def __init__(self, field): 304 | super().__init__(field) 305 | self.spawns = [] 306 | 307 | def add(self, mob): 308 | mob.field = self.field 309 | super().add(mob) 310 | self.spawns.append(mob) 311 | 312 | async def remove(self, key): 313 | mob = self.get(key) 314 | 315 | if mob: 316 | _ = None # owner 317 | 318 | # await self.field.broadcast(CPacket.mob_leave_field(mob)) 319 | 320 | if mob.dead: 321 | pass 322 | # drop_pos_x = mob.position.x 323 | # drop_pos_y = mob.position.y 324 | 325 | # self.field.drops.add(Drop(0, mob.position, 50, 1)) 326 | 327 | return super().remove(key) 328 | 329 | 330 | class NpcPool(ObjectPool): 331 | ... 332 | 333 | 334 | class UserPool(ObjectPool): 335 | def add(self, client): 336 | super().add(client) 337 | client.character.field = self.field 338 | 339 | @property 340 | def characters(self): 341 | return [client.character for client in self] 342 | 343 | def __aiter__(self): 344 | return [client for client in self] 345 | 346 | 347 | class PortalManager: 348 | def __init__(self): 349 | self.portals = [] 350 | 351 | def add(self, portal): 352 | self.portals.append(portal) 353 | 354 | def __filter_portals(self, name): 355 | return filter(lambda p: p.name == name, self.portals) 356 | 357 | def get_portal(self, name): 358 | return next(self.__filter_portals(name), None) 359 | 360 | def get_random_spawn(self): 361 | portals = list(self.__filter_portals("sp")) 362 | 363 | return choice(portals).id if portals else 0 364 | 365 | 366 | class FootholdManager: 367 | def __init__(self): 368 | self.footholds = [] 369 | 370 | def add(self, foothold): 371 | self.footholds.append(foothold) 372 | 373 | def find_below(self, tag_point): 374 | matches = [] 375 | 376 | for foothold in self.footholds: 377 | if foothold.x1 <= tag_point.x and foothold.x2 >= tag_point.x: 378 | matches.append(foothold) 379 | 380 | for foothold in matches: 381 | if not foothold.wall and foothold.y1 != foothold.y2: 382 | s1 = foothold.y2 - foothold.y1 383 | s2 = foothold.x2 - foothold.x1 384 | s4 = tag_point.x - foothold.x1 385 | alpha = atan(s2 / s1) 386 | beta = atan(s1 / s2) 387 | s5 = cos(alpha) * (s4 / cos(beta)) 388 | 389 | if foothold.y2 < foothold.y2: 390 | calcy = foothold.y1 - int(s5) 391 | else: 392 | calcy = foothold.y1 + int(s5) 393 | 394 | if calcy >= tag_point.y: 395 | return foothold 396 | 397 | elif not foothold.wall and foothold.y1 >= tag_point.y: 398 | return foothold 399 | 400 | return None 401 | -------------------------------------------------------------------------------- /mapy/cpacket.py: -------------------------------------------------------------------------------- 1 | from .constants import Network 2 | 3 | from .opcodes import CSendOps 4 | from .packet import Packet 5 | 6 | 7 | class CPacket: 8 | @staticmethod 9 | def check_password_result(pending=None, response=None): 10 | packet = Packet(op_code=CSendOps.LP_CheckPasswordResult) 11 | 12 | if response != 0: 13 | packet.encode_int(response) 14 | packet.encode_short(0) 15 | 16 | if not pending: 17 | return packet 18 | 19 | packet.encode_byte(0) 20 | packet.encode_byte(0) 21 | packet.encode_int(0) 22 | 23 | packet.encode_int(pending.account.id) 24 | packet.encode_byte(pending.account.gender) 25 | packet.encode_byte(0) 26 | packet.encode_short(0) 27 | packet.encode_byte(0) 28 | packet.encode_string(pending.account.username) 29 | packet.encode_byte(0) 30 | packet.encode_byte(0) 31 | packet.encode_long(0) 32 | packet.encode_long(0) 33 | packet.encode_int(4) 34 | 35 | packet.encode_byte(True) 36 | packet.encode_byte(1) 37 | 38 | packet.encode_long(0) 39 | 40 | return packet 41 | 42 | @staticmethod 43 | def world_information(world): 44 | 45 | packet = Packet(op_code=CSendOps.LP_WorldInformation) 46 | packet.encode_byte(world.id) 47 | packet.encode_string(world.name) 48 | packet.encode_byte(2) # 0 : Normal 1 : Event 2 : New 3 : Hot 49 | packet.encode_string("Issa Event") 50 | packet.encode_short(100) 51 | packet.encode_short(100) 52 | packet.encode_byte(False) 53 | 54 | packet.encode_byte(2) 55 | 56 | for i in range(2): 57 | packet.encode_string(f"{world.name}-{i}") 58 | packet.encode_int(100) # Online Count 59 | packet.encode_byte(1) 60 | packet.encode_byte(i) 61 | packet.encode_byte(False) 62 | 63 | packet.encode_short(0) 64 | 65 | return packet 66 | 67 | @staticmethod 68 | def end_world_information(): 69 | packet = Packet(op_code=CSendOps.LP_WorldInformation) 70 | packet.encode_byte(0xFF) 71 | return packet 72 | 73 | @staticmethod 74 | def last_connected_world(world_id): 75 | packet = Packet(op_code=CSendOps.LP_LatestConnectedWorld) 76 | # default: WorldID, 253: None, 255: Recommended World 77 | packet.encode_int(world_id) 78 | return packet 79 | 80 | @staticmethod 81 | def send_recommended_world(worlds): 82 | packet = Packet(op_code=CSendOps.LP_RecommendWorldMessage) 83 | packet.encode_byte(len(worlds)) 84 | 85 | for world in worlds: 86 | packet.encode_int(world.id) 87 | packet.encode_string(world.event_message) 88 | 89 | return packet 90 | 91 | @staticmethod 92 | def check_user_limit(status): 93 | packet = Packet(op_code=CSendOps.LP_CheckUserLimitResult) 94 | 95 | # 0: Open 1: Over user limit 96 | packet.encode_byte(0) 97 | # 0: Normal 1: Highly Populated 2: Full 98 | packet.encode_byte(status) 99 | return packet 100 | 101 | @staticmethod 102 | def world_result(entries): 103 | packet = Packet(op_code=CSendOps.LP_SelectWorldResult) 104 | 105 | packet.encode_byte(0) 106 | packet.encode_byte(len(entries)) 107 | 108 | for entry in entries: 109 | entry.encode(packet) 110 | 111 | packet.encode_byte(2) 112 | packet.encode_int(3) 113 | packet.encode_int(0) 114 | 115 | return packet 116 | 117 | @staticmethod 118 | def check_duplicated_id_result(name, is_available): 119 | packet = Packet(op_code=CSendOps.LP_CheckDuplicatedIDResult) 120 | packet.encode_string(name) 121 | packet.encode_byte(is_available) 122 | return packet 123 | 124 | @staticmethod 125 | def extra_char_info(character): 126 | packet = Packet(op_code=CSendOps.LP_CheckExtraCharInfoResult) 127 | return packet 128 | 129 | @staticmethod 130 | def start_view_all_characters(characters): 131 | packet = Packet(op_code=CSendOps.LP_ViewAllCharResult) 132 | packet.encode_byte(1) 133 | packet.encode_int(2) 134 | packet.encode_int(len(characters)) 135 | return packet 136 | 137 | @staticmethod 138 | def view_all_characters(world, characters): 139 | packet = Packet(op_code=CSendOps.LP_ViewAllCharResult) 140 | 141 | packet.encode_byte(0) 142 | packet.encode_byte(world.id) 143 | 144 | characters = list(filter(lambda ch: ch.world_id == world.id, characters)) 145 | 146 | packet.encode_byte(len(characters)) 147 | 148 | for character in characters: 149 | character.encode_stats(packet) 150 | character.encode_look(packet) 151 | 152 | packet.encode_byte(0) # VAC rank? 153 | 154 | packet.encode_byte(2) 155 | return packet 156 | 157 | @staticmethod 158 | def create_new_character(character, response: bool): 159 | packet = Packet(op_code=CSendOps.LP_CreateNewCharacterResult) 160 | packet.encode_byte(response) 161 | 162 | if not response: 163 | character.encode_entry(packet) 164 | 165 | return packet 166 | 167 | @staticmethod 168 | def select_character_result(uid, port): 169 | packet = Packet(op_code=CSendOps.LP_SelectCharacterResult) 170 | 171 | packet.encode_byte(0) # world 172 | packet.encode_byte(0) # selected char 173 | 174 | packet.encode_buffer(Network.SERVER_ADDRESS) 175 | packet.encode_short(port) 176 | packet.encode_int(uid) 177 | packet.encode_byte(0) 178 | packet.encode_int(0) 179 | 180 | return packet 181 | 182 | # ---------------- Login Server End --------------- # 183 | 184 | @staticmethod 185 | def set_field(character, character_data, channel: int): 186 | packet = Packet(op_code=CSendOps.LP_SetField) 187 | # CPacket.client_opt_man__encode_opt(packet, 0) 188 | packet.encode_short(0) 189 | 190 | packet.encode_int(channel) 191 | packet.encode_int(0) 192 | 193 | packet.encode_byte(1) 194 | packet.encode_byte(character_data) 195 | packet.encode_short(0) 196 | 197 | if character_data: 198 | # character.random.encode(packet) 199 | packet.encode_int(0) 200 | packet.encode_int(0) 201 | packet.encode_int(0) 202 | character.encode(packet) 203 | 204 | packet.encode_int(0) 205 | packet.encode_int(0) 206 | packet.encode_int(0) 207 | packet.encode_int(0) 208 | 209 | else: 210 | packet.encode_byte(0) 211 | packet.encode_int(character.field_id) 212 | packet.encode_byte(character.stats.portal) 213 | packet.encode_int(character.stats.hp) 214 | packet.encode_byte(0) 215 | 216 | packet.encode_long(150842304000000000) 217 | 218 | return packet 219 | 220 | @staticmethod 221 | def func_keys_init(keys): 222 | packet = Packet(op_code=CSendOps.LP_FuncKeyMappedInit) 223 | packet.encode_byte(0) 224 | 225 | for i in range(90): 226 | key = keys[i] 227 | packet.encode_byte(getattr(key, "type", 0)) 228 | packet.encode_int(getattr(key, "action", 0)) 229 | 230 | return packet 231 | 232 | @staticmethod 233 | def set_gender(gender): 234 | packet = Packet(op_code=CSendOps.LP_SetGender) 235 | packet.encode_byte(gender) 236 | return packet 237 | 238 | @staticmethod 239 | def stat_changed(modifier=None, excl_req=False): 240 | packet = Packet(op_code=CSendOps.LP_StatChanged) 241 | packet.encode_byte(excl_req) 242 | if modifier: 243 | modifier.encode(packet) 244 | else: 245 | packet.encode_int(4) 246 | packet.encode_byte(0) 247 | packet.encode_byte(0) 248 | 249 | return packet 250 | 251 | @staticmethod 252 | def enable_actions(): 253 | return CPacket.stat_changed(excl_req=True) 254 | 255 | @staticmethod 256 | def claim_svr_changed(claim_svr_con: bool): 257 | packet = Packet(op_code=CSendOps.LP_ClaimSvrStatusChanged) 258 | packet.encode_byte(claim_svr_con) 259 | return packet 260 | 261 | # ------------------- User Pool ------------------- # 262 | 263 | @staticmethod 264 | def user_enter_field(character): 265 | packet = Packet(op_code=CSendOps.LP_UserEnterField) 266 | packet.encode_int(character.id) 267 | 268 | packet.encode_byte(character.stats.level) 269 | packet.encode_string(character.stats.name) 270 | 271 | packet.skip(8) 272 | 273 | packet.encode_long(0).encode_long(0).encode_byte(0).encode_byte(0) 274 | 275 | packet.encode_short(character.stats.job) 276 | character.encode_look(packet) 277 | 278 | packet.encode_int(0) # driver ID 279 | packet.encode_int(0) # passenger ID 280 | packet.encode_int(0) # choco count 281 | packet.encode_int(0) # active effect item ID 282 | packet.encode_int(0) # completed set item ID 283 | packet.encode_int(0) # portable chair ID 284 | 285 | packet.encode_short(0) # private? 286 | 287 | packet.encode_short(0) 288 | packet.encode_byte(character.position.stance) 289 | packet.encode_short(character.position.foothold) 290 | packet.encode_byte(0) # show admin effect 291 | 292 | packet.encode_byte(0) # pets? 293 | 294 | packet.encode_int(0) # taming mob level 295 | packet.encode_int(0) # taming mob exp 296 | packet.encode_int(0) # taming mob fatigue 297 | 298 | packet.encode_byte(0) # mini room type 299 | 300 | packet.encode_byte(0) # ad board remote 301 | packet.encode_byte(0) # on couple record add 302 | packet.encode_byte(0) # on friend record add 303 | packet.encode_byte(0) # on marriage record add 304 | 305 | packet.encode_byte(0) # some sort of effect bit flag 306 | 307 | packet.encode_byte(0) # new year card record add 308 | packet.encode_int(0) # phase 309 | return packet 310 | 311 | @staticmethod 312 | def user_leave_field(character): 313 | packet = Packet(op_code=CSendOps.LP_UserLeaveField) 314 | packet.encode_int(character.id) 315 | return packet 316 | 317 | @staticmethod 318 | def user_movement(uid, move_path): 319 | packet = Packet(op_code=CSendOps.LP_UserMove) 320 | packet.encode_int(uid) 321 | packet.encode_buffer(move_path) 322 | return packet 323 | 324 | @staticmethod 325 | def effect_remote(src, a, skill_id, skill_level, b): 326 | packet = Packet(op_code=CSendOps.LP_UserEffectRemote) 327 | return packet 328 | 329 | # --------------------- Mob Pool ------------------- # 330 | 331 | @staticmethod 332 | def mob_enter_field(mob): 333 | packet = Packet(op_code=CSendOps.LP_MobEnterField) 334 | mob.encode_init(packet) 335 | return packet 336 | 337 | @staticmethod 338 | def mob_change_controller(mob, level): 339 | packet = Packet(op_code=CSendOps.LP_MobChangeController) 340 | packet.encode_byte(level) 341 | 342 | if level == 0: 343 | packet.encode_int(mob.obj_id) 344 | else: 345 | mob.encode_init(packet) 346 | 347 | return packet 348 | 349 | # --------------------- Npc Pool ----------------------# 350 | 351 | @staticmethod 352 | def npc_enter_field(npc): 353 | packet = Packet(op_code=CSendOps.LP_NpcEnterField) 354 | packet.encode_int(npc.obj_id) 355 | packet.encode_int(npc.life_id) 356 | 357 | packet.encode_short(npc.x) 358 | packet.encode_short(abs(npc.cy)) 359 | packet.encode_byte(npc.f != 1) 360 | packet.encode_short(npc.foothold) 361 | 362 | packet.encode_short(npc.rx0) 363 | packet.encode_short(npc.rx1) 364 | 365 | packet.encode_byte(True) 366 | 367 | return packet 368 | 369 | @staticmethod 370 | def npc_script_message(npc, msg_type, msg, end_bytes, type_, other_npc): 371 | packet = Packet(op_code=CSendOps.LP_ScriptMessage) 372 | 373 | packet.encode_byte(4) 374 | packet.encode_int(npc) 375 | packet.encode_byte(msg_type) 376 | packet.encode_byte(type_) 377 | 378 | if type_ in [4, 5]: 379 | packet.encode_int(other_npc) 380 | 381 | packet.encode_string(msg) 382 | 383 | if end_bytes: 384 | packet.encode(bytes(end_bytes)) 385 | 386 | return packet 387 | 388 | @staticmethod 389 | def broadcast_server_msg(msg): 390 | return CPacket.broadcast_msg(4, msg) 391 | 392 | @staticmethod 393 | def broadcast_msg(type_, msg): 394 | packet = Packet(op_code=CSendOps.LP_BroadcastMsg) 395 | packet.encode_byte(type_) 396 | 397 | if type_ == 4: 398 | packet.encode_byte(True) 399 | 400 | packet.encode_string(msg) 401 | return packet 402 | -------------------------------------------------------------------------------- /mapy/game/character.py: -------------------------------------------------------------------------------- 1 | from attrs import define, field 2 | 3 | from ..client import WvsGameClient 4 | from ..constants import PERMANENT, InventoryType, StatModifiers, is_extend_sp_job 5 | from ..cpacket import CPacket 6 | from ..tools import Random 7 | from .field import Field, FieldObject 8 | from .inventory import Inventory, InventoryManager 9 | 10 | 11 | class MapleCharacter(FieldObject): 12 | def __init__(self, stats: dict | None = None): 13 | super().__init__() 14 | self._client: WvsGameClient | None = None 15 | self._data = None 16 | 17 | if not stats: 18 | stats = {} 19 | 20 | self._field: None | Field = None 21 | self.stats = Stats(**stats) 22 | self.inventories: InventoryManager = InventoryManager(self) 23 | self.func_keys = FuncKeys(self) 24 | self.modify = PlayerModifiers(self) 25 | self.skills = {} 26 | self.random = Random() 27 | 28 | self.map_transfer = [0, 0, 0, 0, 0] 29 | self.map_transfer_ex = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 30 | self.monster_book_cover_id = 0 31 | 32 | @property 33 | def id(self): 34 | return self.stats.id 35 | 36 | @property 37 | def field_id(self): 38 | if self._field: 39 | return self._field.id 40 | return -1 41 | 42 | @property 43 | def field(self): 44 | return self._field 45 | 46 | @property 47 | def client(self): 48 | return self._client 49 | 50 | @client.setter 51 | def client(self, value): 52 | self._client = value 53 | 54 | @property 55 | def data(self): 56 | return self._data 57 | 58 | @data.setter 59 | def data(self, value): 60 | self._data = value 61 | 62 | @property 63 | def equip_inventory(self) -> Inventory: 64 | return self.inventories.get(1) or Inventory(1, 96) 65 | 66 | @property 67 | def consume_inventory(self) -> Inventory: 68 | return self.inventories.get(2) or Inventory(2, 96) 69 | 70 | @property 71 | def install_inventory(self) -> Inventory: 72 | return self.inventories.get(3) or Inventory(3, 96) 73 | 74 | @property 75 | def etc_inventory(self) -> Inventory: 76 | return self.inventories.get(4) or Inventory(4, 96) 77 | 78 | @property 79 | def cash_inventory(self) -> Inventory: 80 | return self.inventories.get(5) or Inventory(5, 96) 81 | 82 | def encode_entry(self, packet): 83 | ranking = False 84 | 85 | self.stats.encode(packet) 86 | self.encode_look(packet) 87 | packet.encode_byte(0) 88 | packet.encode_byte(0) 89 | 90 | if ranking: 91 | packet.skip(16) 92 | 93 | def encode(self, packet): 94 | packet.encode_long(-1 & 0xFFFFFFFF) 95 | packet.encode_byte(0) # combat orders 96 | packet.encode_byte(0) 97 | 98 | self.stats.encode(packet) 99 | packet.encode_byte(100) # Buddylist capacity 100 | packet.encode_byte(False) 101 | packet.encode_int(self.stats.money) 102 | 103 | self.encode_inventories(packet) 104 | self.encode_skills(packet) 105 | self.encode_quests(packet) 106 | self.encode_minigames(packet) 107 | self.encode_rings(packet) 108 | self.encode_teleports(packet) 109 | # self.encode_monster_book(packet) 110 | self.encode_new_year(packet) 111 | packet.encode_short(0) 112 | # self.encode_area(packet) 113 | packet.encode_short(0) 114 | packet.encode_short(0) 115 | 116 | def encode_inventories(self, packet): 117 | packet.encode_byte(self.equip_inventory.slots) 118 | packet.encode_byte(self.consume_inventory.slots) 119 | packet.encode_byte(self.install_inventory.slots) 120 | packet.encode_byte(self.etc_inventory.slots) 121 | packet.encode_byte(self.cash_inventory.slots) 122 | 123 | packet.encode_int(0) 124 | packet.encode_int(0) 125 | 126 | equipped = {} 127 | 128 | for index, item in self.equip_inventory.items.items(): 129 | if index < 0: 130 | equipped[index] = self.equip_inventory[index] 131 | 132 | stickers, eqp_normal = {}, {} 133 | 134 | if equipped.get(-11): 135 | eqp_normal[-11] = equipped.pop(-11) 136 | 137 | for index, item in equipped.items(): 138 | if index > -100 and equipped.get(index - 100): 139 | eqp_normal[index] = item 140 | 141 | else: 142 | new_index = index + 100 if index < -100 else index 143 | stickers[new_index] = item 144 | 145 | inv_equip = { 146 | slot: item for slot, item in self.equip_inventory.items.items() if slot >= 0 147 | } 148 | dragon_equip = { 149 | slot: item 150 | for slot, item in self.equip_inventory.items.items() 151 | if slot >= -1100 and slot < -1000 152 | } 153 | mechanic_equip = { 154 | slot: item 155 | for slot, item in self.equip_inventory.items.items() 156 | if slot >= -1200 and slot < -1100 157 | } 158 | 159 | for inv in [eqp_normal, stickers, inv_equip, dragon_equip, mechanic_equip]: 160 | for slot, item in inv.items(): 161 | if not item: 162 | continue 163 | 164 | packet.encode_short(abs(slot)) 165 | item.encode(packet) 166 | 167 | packet.encode_short(0) 168 | 169 | self.consume_inventory.encode(packet) 170 | self.install_inventory.encode(packet) 171 | self.etc_inventory.encode(packet) 172 | self.cash_inventory.encode(packet) 173 | 174 | def encode_skills(self, packet): 175 | packet.encode_short(len(self.skills)) 176 | for _, skill in self.skills.items(): 177 | skill.encode(packet) 178 | 179 | if False: 180 | packet.encode_int(skill.mastery_level) # is skill needed for mastery 181 | 182 | packet.encode_short(0) 183 | 184 | def encode_quests(self, packet): 185 | packet.encode_short(0) 186 | 187 | packet.encode_short(0) 188 | 189 | def encode_minigames(self, packet): 190 | packet.encode_short(0) 191 | 192 | def encode_rings(self, packet): 193 | packet.encode_short(0) 194 | packet.encode_short(0) 195 | packet.encode_short(0) 196 | 197 | # Maybe needs to not be filled by default 198 | def encode_teleports(self, packet): 199 | for _ in range(5): 200 | packet.encode_int(0) 201 | 202 | for _ in range(10): 203 | packet.encode_int(0) 204 | 205 | def encode_monster_book(self, packet): 206 | packet.encode_int(self.monster_book_cover_id) 207 | packet.encode_byte(0) 208 | 209 | packet.encode_short(0) 210 | 211 | def encode_new_year(self, packet): 212 | packet.encode_short(0) 213 | 214 | def encode_area(self, packet): 215 | packet.encode_short(0) 216 | 217 | def encode_look(self, packet): 218 | packet.encode_byte(self.stats.gender) 219 | packet.encode_byte(self.stats.skin) 220 | packet.encode_int(self.stats.face) 221 | packet.encode_byte(0) 222 | packet.encode_int(self.stats.hair) 223 | 224 | inventory: Inventory = self.inventories.get(InventoryType.EQUIP) 225 | equipped = {} 226 | 227 | for index in inventory.items: 228 | if index < 0: 229 | equipped[index] = inventory.items[index] 230 | 231 | stickers, unseen = {}, {} 232 | 233 | for index, item in equipped.items(): 234 | if index > -100 and equipped.get(index - 100): 235 | unseen[index] = item 236 | 237 | else: 238 | new_index = index + 100 if index < -100 else index 239 | stickers[new_index] = item 240 | 241 | for inv in [stickers, unseen]: 242 | for index, item in inv.items(): 243 | packet.encode_byte(index * -1).encode_int(item.item_id) 244 | 245 | packet.encode_byte(0xFF) 246 | 247 | packet.encode_int(0 if not equipped.get(-111) else equipped[-111].item_id) 248 | 249 | # for pet_id in self.pet_ids: 250 | for pet_id in range(3): 251 | packet.encode_int(pet_id) 252 | 253 | async def send_packet(self, packet): 254 | if not self._client: 255 | raise ConnectionError 256 | 257 | await self._client.send_packet(packet) 258 | 259 | 260 | class CharacterEntry: 261 | def __init__(self, **stats): 262 | self._stats = Stats(**stats) 263 | self._equip = Inventory(InventoryType.EQUIP, 96) 264 | 265 | @property 266 | def id(self): 267 | return self._stats.id 268 | 269 | @property 270 | def stats(self): 271 | return self._stats 272 | 273 | @property 274 | def equip(self): 275 | return self._equip 276 | 277 | def encode(self, packet): 278 | ranking = False 279 | 280 | self._stats.encode(packet) 281 | self.encode_look(packet) 282 | packet.encode_byte(0) # VAC 283 | packet.encode_byte(ranking) 284 | 285 | if ranking: 286 | packet.skip(16) 287 | 288 | def encode_look(self, packet): 289 | packet.encode_byte(self.stats.gender) 290 | packet.encode_byte(self.stats.skin) 291 | packet.encode_int(self.stats.face) 292 | packet.encode_byte(0) 293 | packet.encode_int(self.stats.hair) 294 | 295 | equipped = {} 296 | 297 | for index, item in self.equip.items.items(): 298 | if index < 0: 299 | equipped[index] = self.equip[index] 300 | 301 | stickers, eqp_normal = {}, {} 302 | 303 | if equipped.get(-11): 304 | eqp_normal[-11] = equipped.pop(-11) 305 | 306 | for index, item in equipped.items(): 307 | if index > -100 and equipped.get(index - 100): 308 | eqp_normal[index] = item 309 | 310 | else: 311 | new_index = index + 100 if index < -100 else index 312 | stickers[new_index] = item 313 | 314 | for inv in [stickers, eqp_normal]: 315 | for slot, item in inv.items(): 316 | packet.encode_byte(abs(slot)) 317 | packet.encode_int(item.item_id) 318 | 319 | packet.encode_byte(0xFF) 320 | 321 | packet.encode_int(0 if not equipped.get(-111) else equipped[-111].item_id) 322 | 323 | # for pet_id in self.pet_ids: 324 | for pet_id in range(3): 325 | packet.encode_int(pet_id) 326 | 327 | 328 | @define 329 | class FuncKey: 330 | type: int 331 | action: int 332 | 333 | 334 | class FuncKeys: 335 | def __init__(self, character): 336 | self._parent = character 337 | self._func_keys = {} 338 | 339 | def __setitem__(self, key, value): 340 | self._func_keys[key] = value 341 | 342 | def __getitem__(self, key): 343 | return self._func_keys.get(key, FuncKey(0, 0)) 344 | 345 | 346 | class Skills(dict): 347 | def __init__(self, parent): 348 | self._parent = parent 349 | 350 | async def cast(self, skill_id): 351 | skill = self.get(skill_id) 352 | 353 | if not skill: 354 | return False 355 | 356 | await self._parent.modify.stats( 357 | hp=self._parent.stats.hp - 1, mp=self._parent.stats.mp - 1 358 | ) 359 | 360 | # if skill.level_data.buff_time > 0: 361 | # await self._parent.buffs.remove(skill.id) 362 | 363 | # buff = Buff(skill.id) 364 | # buff.generate(skill.level_data) 365 | 366 | # await self._parent.buff.add(buff) 367 | 368 | return True 369 | 370 | 371 | def default_extend_sp(): 372 | return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 373 | 374 | 375 | def default_pet_locker(): 376 | return [0, 0, 0] 377 | 378 | 379 | @define 380 | class Stats(object): 381 | id: int = 0 382 | name: str = "" 383 | world_id: int = 0 384 | 385 | gender: int = 0 386 | skin: int = 0 387 | face: int = field(default=20001) 388 | hair: int = field(default=30003) 389 | level: int = field(default=1) 390 | job: int = 0 391 | 392 | _str: int = field(default=4) 393 | dex: int = field(default=4) 394 | _int: int = field(default=4) 395 | luk: int = field(default=4) 396 | hp: int = field(default=50) 397 | m_hp: int = field(default=50) 398 | mp: int = field(default=5) 399 | m_mp: int = field(default=5) 400 | 401 | ap: int = 0 402 | sp: int = 0 403 | extend_sp: list[int] = field(factory=lambda: list(bytearray(10))) 404 | 405 | exp: int = 0 406 | money: int = 0 407 | fame: int = 0 408 | temp_exp: int = 0 409 | 410 | field_id: int = field(default=100000000) 411 | portal: int = 0 412 | play_time: int = 0 413 | sub_job: int = 0 414 | pet_locker: list[int] = field(factory=lambda: list(bytearray(3))) 415 | 416 | def encode(self, packet) -> None: 417 | packet.encode_int(self.id) 418 | packet.encode_fixed_string(self.name, 13) 419 | packet.encode_byte(self.gender) 420 | packet.encode_byte(self.skin) 421 | packet.encode_int(self.face) 422 | packet.encode_int(self.hair) 423 | 424 | for sn in self.pet_locker: 425 | packet.encode_long(sn) 426 | 427 | packet.encode_byte(self.level) 428 | packet.encode_short(self.job) 429 | packet.encode_short(self._str) 430 | packet.encode_short(self.dex) 431 | packet.encode_short(self._int) 432 | packet.encode_short(self.luk) 433 | packet.encode_int(self.hp) 434 | packet.encode_int(self.m_hp) 435 | packet.encode_int(self.mp) 436 | packet.encode_int(self.m_mp) 437 | packet.encode_short(self.ap) 438 | 439 | # if player not evan 440 | packet.encode_short(self.sp) 441 | # else 442 | # packet.encode_byte(len(self.extend_sp)) 443 | 444 | # for i, sp in enumerate(self.extend_sp): 445 | # packet.encode_byte(i) 446 | # packet.encode_byte(sp) 447 | 448 | packet.encode_int(self.exp) 449 | packet.encode_short(self.fame) 450 | packet.encode_int(self.temp_exp) 451 | packet.encode_int(self.field_id) 452 | packet.encode_byte(self.portal) 453 | packet.encode_int(self.play_time) 454 | packet.encode_short(self.sub_job) 455 | 456 | 457 | class StatModifier: 458 | def __init__(self, character_stats): 459 | self._modifiers = [] 460 | self._stats = character_stats 461 | 462 | @property 463 | def modifiers(self): 464 | return self._modifiers 465 | 466 | @property 467 | def flag(self): 468 | _flag = 0 469 | for mod in self._modifiers: 470 | _flag |= mod.value 471 | return _flag 472 | 473 | def alter(self, **stats): 474 | for key, val in stats.items(): 475 | modifier = StatModifiers[key.upper()] 476 | self._modifiers.append(modifier) 477 | setattr(self._stats, key, val) 478 | 479 | def encode(self, packet): 480 | packet.encode_int(self.flag) 481 | 482 | for modifier in StatModifiers: 483 | if modifier not in self._modifiers: 484 | continue 485 | 486 | if modifier is StatModifiers.SP: 487 | if is_extend_sp_job(self._stats.job): 488 | packet.encode_byte(0) 489 | else: 490 | packet.encode_short(self._stats.sp) 491 | else: 492 | getattr(packet, f"encode_{modifier.encode}")( 493 | packet, getattr(self._stats, modifier.name.lower()) 494 | ) 495 | 496 | 497 | class PlayerModifiers: 498 | def __init__(self, character): 499 | self._parent = character 500 | 501 | async def stats(self, *, excl_req=True, **stats): 502 | modifier = StatModifier(self._parent.stats) 503 | modifier.alter(**stats) 504 | 505 | if modifier.modifiers: 506 | await self._parent.send_packet(CPacket.stat_changed(modifier, excl_req)) 507 | 508 | 509 | @define(kw_only=True) 510 | class SkillEntry(object): 511 | id: int = int() 512 | level: int = int() 513 | mastery_level: int = int() 514 | max_level: int = int() 515 | expiration: int = PERMANENT 516 | level_data: list = field(factory=lambda: list()) 517 | 518 | def encode(self, packet): 519 | packet.encode_int(self.id) 520 | packet.encode_int(self.level) 521 | packet.encode_long(PERMANENT) # skill.expiration 522 | 523 | 524 | @define 525 | class Account: 526 | id: int = 0 527 | username: str = "" 528 | password: str = "" 529 | gender: int = 0 530 | creation: str = "" 531 | last_login: str = "" 532 | last_ip: str = "127.0.0.1" 533 | ban: int = 0 534 | admin: int = 0 535 | last_connected_world: int = 0 536 | -------------------------------------------------------------------------------- /mapy/database/structure.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, EnumMeta 2 | 3 | 4 | class Meta(EnumMeta): 5 | def __str__(self): 6 | return f"{self.__class__.__name__.lower()}.{super().__str__()}" 7 | 8 | 9 | # class Schema(object): 10 | # def __init_subclass__(cls, *args, **kwargs): 11 | # for named, type_ in cls.__annotations__.items(): 12 | # anon_prop = property( 13 | # fget=lambda x: getattr(cls, f"_{x}_"), 14 | # fset=lambda x, v: setattr(cls, f"_{x}_", v), 15 | # ) 16 | # setattr(cls, named, anon_prop) 17 | 18 | 19 | class Schema(Meta): 20 | def __new__(cls, value): 21 | cls.schema = cls.__name__.lower() 22 | cls.primary_key = value.__dict__.get("__primary_key__") 23 | cls.foreign_keys = value.__dict__.get("__foreign_keys__") 24 | cls.columns = [item.lower() for item in value._member_names_] 25 | super().__new__(cls, value) 26 | return cls 27 | 28 | def __str__(self): 29 | return f"{self.__class__.__name__.lower()}.{super().__str__()}" 30 | 31 | def __getattr__(self, name): 32 | if name not in ("_value_"): 33 | return getattr(self.value, name) 34 | return super(type(Meta)).__getattr__(name) 35 | 36 | @classmethod 37 | def create(cls): 38 | ... 39 | 40 | 41 | class Table(int, Enum, metaclass=Meta): 42 | _ignore_ = "data_type" 43 | 44 | def __new__(cls, data_type, options=None): 45 | value = len(cls._member_names_) + 1 46 | 47 | cls._value_ = value 48 | cls.data_type = data_type 49 | 50 | cls.options = options if options else {} 51 | enum_class = super().__new__(cls, value) 52 | return enum_class 53 | 54 | 55 | class ItemConsumeableData(Table): 56 | ITEM_ID = "integer" 57 | FLAGS = "varchar[]" 58 | CURES = "varchar[]" 59 | HP = "smallint" 60 | HP_PERCENTAGE = "smallint" 61 | MAX_HP_PERCENTAGE = "smallint" 62 | MP = "smallint" 63 | MP_PERCENTAGE = "smallint" 64 | MAX_MP_PERCENTAGE = "smallint" 65 | WEAPON_ATTACK = "smallint" 66 | WEAPON_ATTACK_PERCENTAGE = "smallint" 67 | WEAPON_DEFENSE = "smallint" 68 | WEAPON_DEFENSE_PERCENTAGE = "smallint" 69 | MAGIC_ATTACK = "smallint" 70 | MAGIC_ATTACK_PERCENTAGE = "smallint" 71 | MAGIC_DEFENSE = "smallint" 72 | MAGIC_DEFENSE_PERCENTAGE = "smallint" 73 | ACCURACY = "smallint" 74 | ACCURACY_PERCENTAGE = "smallint" 75 | AVOID = "smallint" 76 | AVOID_PERCENTAGE = "smallint" 77 | SPEED = "smallint" 78 | SPEED_PERCENTAGE = "smallint" 79 | JUMP = "smallint" 80 | SECONDARY_STAT = "jsonb" 81 | BUFF_TIME = "integer" 82 | PROB = "smallint" 83 | EVENT_POINT = "integer" 84 | MOB_ID = "integer" 85 | MOB_HP = "integer" 86 | SCREEN_MESSAGE = "text" 87 | ATTACK_INDEX = "smallint" 88 | ATTACK_MOB_ID = "integer" 89 | MOVE_TO = "integer" 90 | RETURN_MAP_QR = "integer" 91 | DECREASE_HUNGER = "smallint" 92 | MORPH = "smallint" 93 | CARNIVAL_TYPE = "smallint" 94 | CARNIVAL_POINTS = "smallint" 95 | CARNIVAL_SKILL = "smallint" 96 | EXPERIENCE = "integer" 97 | 98 | __primary_key__ = ("ITEM_ID",) 99 | 100 | 101 | class ItemData(Table): 102 | ITEM_ID = "integer" 103 | INVENTORY = "smallint" 104 | PRICE = ("integer", {"default": 0}) 105 | MAX_SLOT_QUANTITY = "smallint" 106 | MAX_POSSESSION_COUNT = "smallint" 107 | MIN_LEVEL = "smallint" 108 | MAX_LEVEL = ("smallint", {"default": 250}) 109 | EXPERIENCE = "integer" 110 | MONEY = "integer" 111 | STATE_CHANGE_ITEM = "integer" 112 | LEVEL_FOR_MAKER = "smallint" 113 | NPC = "integer" 114 | FLAGS = ("varchar[]", {"default": "'{}'"}) 115 | PET_LIFE_EXTEND = "smallint" 116 | MAPLE_POINT = "integer" 117 | MONEY_MIN = "integer" 118 | MONEY_MAX = "integer" 119 | EXP_RATE = "double" 120 | ADD_TIME = "smallint" 121 | SLOT_INDEX = "smallint" 122 | 123 | __primary_key__ = ("ITEM_ID",) 124 | 125 | 126 | class ItemEquipData(Table): 127 | ITEM_ID = "integer" 128 | FLAGS = ("varchar[]", {"default": "'{}'"}) 129 | EQUIP_SLOTS = ("varchar[]", {"default": "'{}'"}) 130 | ATTACK_SPEED = ("smallint", {"default": 0}) 131 | RUC = ("smallint", {"default": 0}) 132 | REQ_STR = ("smallint", {"default": 0}) 133 | REQ_DEX = ("smallint", {"default": 0}) 134 | REQ_INT = ("smallint", {"default": 0}) 135 | REQ_LUK = ("smallint", {"default": 0}) 136 | REQ_FAME = ("smallint", {"default": 0}) 137 | REQ_JOB = ("smallint[]", {"default": "'{}'"}) 138 | HP = ("smallint", {"default": 0}) 139 | HP_PERCENTAGE = ("smallint", {"default": 0}) 140 | MP = ("smallint", {"default": 0}) 141 | MP_PERCENTAGE = ("smallint", {"default": 0}) 142 | STR = ("smallint", {"default": 0}) 143 | DEX = ("smallint", {"default": 0}) 144 | INT = ("smallint", {"default": 0}) 145 | LUK = ("smallint", {"default": 0}) 146 | HANDS = ("smallint", {"default": 0}) 147 | WEAPON_ATTACK = ("smallint", {"default": 0}) 148 | WEAPON_DEFENSE = ("smallint", {"default": 0}) 149 | MAGIC_ATTACK = ("smallint", {"default": 0}) 150 | MAGIC_DEFENSE = ("smallint", {"default": 0}) 151 | ACCURACY = ("smallint", {"default": 0}) 152 | AVOID = ("smallint", {"default": 0}) 153 | JUMP = ("smallint", {"default": 0}) 154 | SPEED = ("smallint", {"default": 0}) 155 | TRACTION = ("double", {"default": 0}) 156 | RECOVERY = ("double", {"default": 0}) 157 | KNOCKBACK = ("smallint", {"default": 0}) 158 | TAMING_MOB = ("smallint", {"default": 0}) 159 | DURABILITY = ("integer", {"default": "'-1'::integer"}) 160 | INC_LIGHTNING_DAMAGE = ("smallint", {"default": 0}) 161 | INC_ICE_DAMAGE = ("smallint", {"default": 0}) 162 | INC_FIRE_DAMAGE = ("smallint", {"default": 0}) 163 | INC_POISON_DAMAGE = ("smallint", {"default": 0}) 164 | ELEMENTAL_DEFAULT = ("smallint", {"default": 0}) 165 | CRAFT = ("smallint", {"default": 0}) 166 | SET_ID = ("integer", {"default": 0}) 167 | ENCHANT_CATEGORY = ("smallint", {"default": 0}) 168 | HEAL_HP = ("smallint", {"default": 0}) 169 | SPECIAL_ID = ("integer", {"default": 0}) 170 | 171 | __primary_key__ = "ITEM_ID" 172 | 173 | 174 | class ItemPetData(Table): 175 | ITEM_ID = "integer" 176 | HUNGER = "smallint" 177 | LIFE = "smallint" 178 | LIMITED_LIFE = "integer" 179 | EVOLUTION_ITEM = "integer" 180 | EVOLUTION_LEVEL = "smallint" 181 | FLAGS = "varchar[]" 182 | 183 | __primary_key__ = ("ITEM_ID",) 184 | 185 | 186 | class ItemRechargeableData(Table): 187 | ITEM_ID = "integer" 188 | UNIT_PRICE = "smallint" 189 | WEAPON_ATTACK = "smallint" 190 | 191 | __primary_key__ = "ITEM_ID" 192 | 193 | 194 | class MapData(Table): 195 | MAP_ID = "integer" 196 | MAP_NAME = ("text", {"default": "''::text"}) 197 | STREET_NAME = ("text", {"default": "''::text"}) 198 | MAP_MARK = ("text", {"default": "''::text"}) 199 | FLAGS = ("varchar[]", {"default": "'{}'"}) 200 | MOB_RATE = ("double", {"default": 1.0}) 201 | FIXED_MOB_CAPACITY = "smallint" 202 | SPAWN_MOB_INTERVAL = "smallint" 203 | DROP_RATE = ("double", {"default": 1.0}) 204 | REGEN_RATE = ("double", {"default": 1.0}) 205 | SHUFFLE_NAME = ("text", {"default": "NULL::varchar"}) 206 | DEFAULT_BGM = ("text", {"default": "NULL::varchar"}) 207 | EFFECT = ("text", {"default": "NULL::varchar"}) 208 | MIN_LEVEL_LIMIT = "smallint" 209 | MAX_LEVEL_LIMIT = "smallint" 210 | TIME_LIMIT = "smallint" 211 | DEFAULT_TRACTION = ("double", {"default": 1}) 212 | MAP_LTX = "smallint" 213 | MAP_LTY = "smallint" 214 | MAP_RBX = "smallint" 215 | MAP_RBY = "smallint" 216 | LP_BOTTOM = "smallint" 217 | LP_TOP = "smallint" 218 | LP_SIDE = "smallint" 219 | FORCED_MAP_RETURN = "integer" 220 | FIELD_TYPE = "smallint" 221 | DECREASE_HP = "integer" 222 | DECREASE_MP = "integer" 223 | DECREASE_INTERVAL = "smallint" 224 | DAMAGE_PER_SECOND = "smallint" 225 | PROTECT_ITEM = "integer" 226 | SHIP_KIND = "smallint" 227 | CONSUME_COOLDOWN = "smallint" 228 | LINK = "integer" 229 | FIELD_LIMITATIONS = ("bigint", {"default": 0}) 230 | RETURN_MAP = "integer" 231 | MAP_LIMIT = "smallint" 232 | MAP_VERSION = "smallint" 233 | ON_FIRST_USER_ENTER = "text" 234 | ON_USER_ENTER = "text" 235 | DESCRIPTION = ("text", {"default": "''::text"}) 236 | MOVE_LIMIT = "smallint" 237 | PROTECT_SET = "integer" 238 | ALLOWED_ITEM_DROP = "integer" 239 | DROP_EXPIRE = "smallint" 240 | TIME_OUT_LIMIT = "smallint" 241 | PHASE_ALPHA = "smallint" 242 | PHASE_BACKGROUND = "smallint" 243 | TIME_MOB_SPAWN = "smallint" 244 | 245 | __primary_key__ = "MAP_ID" 246 | 247 | 248 | class MapFootholds(Table): 249 | MAP_ID = "integer" 250 | GROUP_ID = "smallint" 251 | ID = "smallint" 252 | X_1 = "smallint" 253 | Y_1 = "smallint" 254 | X_2 = "smallint" 255 | Y_2 = "smallint" 256 | DRAG_FORCE = "smallint" 257 | PREVIOUS_ID = "smallint" 258 | NEXT_ID = "smallint" 259 | FLAGS = "varchar[]" 260 | 261 | __primary_key__ = ("MAP_ID", "GROUP_ID", "ID") 262 | 263 | 264 | class MapLife(Table): 265 | MAP_ID = "integer" 266 | LIFE_ID = "integer" 267 | LIFE_TYPE = "text" 268 | LIFE_NAME = ("text", {"default": "''::text"}) 269 | X_POS = "smallint" 270 | Y_POS = "smallint" 271 | FOOTHOLD = "smallint" 272 | MIN_CLICK_POS = "smallint" 273 | MAX_CLICK_POS = "smallint" 274 | RESPAWN_TIME = "integer" 275 | FLAGS = ("varchar[]", {"default": "'{}'"}) 276 | CY = "smallint" 277 | FACE = ("boolean", {"default": 0}) 278 | HIDE = ("boolean", {"default": 0}) 279 | 280 | __primary_key__ = None 281 | 282 | 283 | class MapPortals(Table): 284 | MAP_ID = "integer" 285 | ID = "smallint" 286 | NAME = "text" 287 | X_POS = "smallint" 288 | Y_POS = "smallint" 289 | DESTINATION = "integer" 290 | DESTINATION_LABEL = "text" 291 | PORTAL_TYPE = "smallint" 292 | 293 | __primary_key__ = ("MAP_ID", "ID") 294 | 295 | 296 | class MapReactors(Table): 297 | MAP_ID = "integer" 298 | REACTOR_ID = "integer" 299 | REACTOR_TIME = "integer" 300 | NAME = "text" 301 | X = "smallint" 302 | Y = "smallint" 303 | F = "smallint" 304 | 305 | __primary_key__ = ("MAP_ID", "REACTOR_ID") 306 | 307 | 308 | class MapTimedMobs(Table): 309 | MAP_ID = "integer" 310 | MOB_ID = "integer" 311 | START_HOUR = "smallint" 312 | END_HOUR = "smallint" 313 | MESSAGE = "text" 314 | 315 | __primary_key__ = ("MAP_ID", "MOB_ID") 316 | 317 | 318 | class MobData(Table): 319 | MOB_ID = "integer" 320 | MOB_LEVEL = "smallint" 321 | FLAGS = ("varchar[]", {"default": "'{}'"}) 322 | HP = "integer" 323 | HP_RECOVERY = "integer" 324 | MP = "integer" 325 | MP_RECOVERY = "integer" 326 | EXPERIENCE = "integer" 327 | KNOCKBACK = "integer" 328 | FIXED_DAMAGE = "integer" 329 | EXPLODE_HP = "integer" 330 | LINK = "integer" 331 | SUMMON_TYPE = "smallint" 332 | DEATH_BUFF = "integer" 333 | DEATH_AFTER = "integer" 334 | TRACTION = ("double", {"default": 1.0}) 335 | DAMAGED_BY_SKILL_ONLY = "smallint" 336 | DAMAGED_BY_MOB_ONLY = "integer" 337 | DROP_ITEM_PERIOD = "smallint" 338 | HP_BAR_COLOR = "smallint" 339 | HP_BAR_BG_COLOR = "smallint" 340 | CARNIVAL_POINTS = "smallint" 341 | PHYSICAL_ATTACK = "smallint" 342 | PHYSICAL_DEFENSE = "smallint" 343 | PHYSICAL_DEFENSE_RATE = "smallint" 344 | MAGICAL_ATTACK = "smallint" 345 | MAGICAL_DEFENSE = "smallint" 346 | MAGICAL_DEFENSE_RATE = "smallint" 347 | ACCURACY = "smallint" 348 | AVOID = "smallint" 349 | SPEED = "smallint" 350 | FLY_SPEED = "smallint" 351 | CHASE_SPEED = "smallint" 352 | ICE_MODIFIER = "smallint" 353 | FIRE_MODIFIER = "smallint" 354 | POISON_MODIFIER = "smallint" 355 | LIGHTNING_MODIFIER = "smallint" 356 | HOLY_MODIFIER = "smallint" 357 | DARK_MODIFIER = "smallint" 358 | NONELEMENTAL_MODIFIER = "smallint" 359 | 360 | __primary_key__ = "MOB_ID" 361 | 362 | 363 | class MobSkills(Table): 364 | MOB_ID = "integer" 365 | SKILL_ID = "smallint" 366 | LEVEL = "smallint" 367 | EFFECT_AFTER = "smallint" 368 | 369 | __primary_key__ = ("MOB_ID", "SKILL_ID") 370 | 371 | 372 | class PlayerSkillData(Table): 373 | SKILL_ID = "integer" 374 | FLAGS = "varchar[]" 375 | WEAPON = "smallint" 376 | SUB_WEAPON = "smallint" 377 | MAX_LEVEL = "smallint" 378 | BASE_MAX_LEVEL = "smallint" 379 | SKILL_TYPE = "varchar[]" 380 | ELEMENT = "text" 381 | MOB_COUNT = "text" 382 | HIT_COUNT = "text" 383 | BUFF_TIME = "text" 384 | HP_COST = "text" 385 | MP_COST = "text" 386 | DAMAGE = "text" 387 | FIXED_DAMAGE = "text" 388 | CRITICAL_DAMAGE = "text" 389 | MASTERY = "text" 390 | OPTIONAL_ITEM_COST = "smallint" 391 | ITEM_COST = "integer" 392 | ITEM_COUNT = "smallint" 393 | BULLET_COST = "smallint" 394 | MONEY_COST = "text" 395 | X_PROPERTY = "text" 396 | Y_PROPERTY = "text" 397 | SPEED = "text" 398 | JUMP = "text" 399 | STR = "text" 400 | WEAPON_ATTACK = "text" 401 | WEAPON_DEFENSE = "text" 402 | MAGIC_ATTACK = "text" 403 | MAGIC_DEFENSE = "text" 404 | ACCURACY = "text" 405 | AVOID = "text" 406 | HP = "text" 407 | MP = "text" 408 | PROBABILITY = "text" 409 | LTX = "smallint" 410 | LTY = "smallint" 411 | RBX = "smallint" 412 | RBY = "smallint" 413 | COOLDOWN_TIME = "text" 414 | AVOID_CHANCE = "text" 415 | RANGE = "text" 416 | MORPH = "smallint" 417 | 418 | __primary_key__ = "SKILL_ID" 419 | 420 | 421 | class PlayerSkillRequirementData(Table): 422 | SKILL_ID = "integer" 423 | REQ_SKILL_ID = "integer" 424 | REQ_LEVEL = "smallint" 425 | 426 | __primary_key__ = ("SKILL_ID", "REQ_SKILL_ID") 427 | 428 | 429 | class String(Table): 430 | OBJECT_ID = "integer" 431 | OBJECT_TYPE = "varchar" 432 | DESCRIPTION = "text" 433 | LABEL = "text" 434 | 435 | __primary_key__ = ("OBJECT_ID", "OBJECT_TYPE") 436 | 437 | 438 | class RMDB(Schema): 439 | ITEM_CONSUMEABLE_DATA = ItemConsumeableData 440 | ITEM_DATA = ItemData 441 | ITEM_EQUIP_DATA = ItemEquipData 442 | ITEM_PET_DATA = ItemPetData 443 | ITEM_RECHARGEABLE_DATA = ItemRechargeableData 444 | MAP_DATA = MapData 445 | MAP_FOOTHOLDS = MapFootholds 446 | MAP_LIFE = MapLife 447 | MAP_PORTALS = MapPortals 448 | MAP_REACTORS = MapReactors 449 | MAP_TIMED_MOBS = MapTimedMobs 450 | MOB_DATA = MobData 451 | MOB_SKILLS = MobSkills 452 | PLAYER_SKILL_DATA = PlayerSkillData 453 | PLAYER_SKILL_REQS_DATA = PlayerSkillRequirementData 454 | STRING = String 455 | 456 | 457 | class Account(Table): 458 | ID = "serial" 459 | USERNAME = "text" 460 | PASSWORD = "text" 461 | SALT = "text" 462 | CREATION = ("date", {"default": "CURRENT_DATE"}) 463 | LAST_LOGIN = ("date", {"default": '"1970-01-01"::date'}) 464 | LAST_IP = ("inet", {"default": '"0.0.0.0"::inet'}) 465 | BAN = ("bool", {"default": 0}) 466 | ADMIN = ("bool", {"default": 0}) 467 | GENDER = "bool" 468 | 469 | __primary_key__ = ("ID",) 470 | # __unqiue_index__ = ("USERNAME") 471 | 472 | 473 | class Buddies(Table): 474 | pass 475 | 476 | 477 | class Character(Table): 478 | ID = "integer" 479 | ACCOUNT_ID = "integer" 480 | NAME = "text" 481 | GENDER = "bool" 482 | SKIN = "smallint" 483 | FACE = "smallint" 484 | HAIR = "smallint" 485 | PET_LOCKER = "int[]" 486 | LEVEL = "smallint" 487 | JOB = "smallint" 488 | STR = "smallint" 489 | DEX = "smallint" 490 | INT = "smallint" 491 | LUK = "smallint" 492 | HP = "smallint" 493 | MAX_HP = "smallint" 494 | MP = "smallint" 495 | MAX_MP = "smallint" 496 | AP = "smallint" 497 | SP = "smallint" 498 | EXP = "integer" 499 | MONEY = "integer" 500 | TEMP_EXP = "integer" 501 | FIELD_ID = "integer" 502 | PORTAL = "text" 503 | PLAY_TIME = "integer" 504 | SUB_JOB = "smallint" 505 | FAME = "smallint" 506 | EXTEND_SP = "smallint[]" 507 | WORLD_ID = "smallint" 508 | 509 | __primary_key__ = ("ID",) 510 | __foreign_keys__ = {"account_id": "accounts.id"} 511 | 512 | 513 | class InventoryEquipment(Table): 514 | INVENTORY_ITEM_ID = "bigint" 515 | UPGRADE_SLOTS = "integer" 516 | STR = "smallint" 517 | DEX = "smallint" 518 | INT = "smallint" 519 | LUK = "smallint" 520 | HP = "integer" 521 | MP = "integer" 522 | WEAPON_ATTACK = "smallint" 523 | MAGIC_ATTACK = "smallint" 524 | WEAPON_DEFENSE = "smallint" 525 | MAGIC_DEFENSE = "smallint" 526 | ACCURACY = "smallint" 527 | AVOID = "smallint" 528 | HANDS = "smallint" 529 | SPEED = "smallint" 530 | JUMP = "smallint" 531 | RING_ID = "integer" 532 | CRAFT = "smallint" 533 | ATTRIBUTE = "smallint" 534 | LEVEL_UP_TYPE = "smallint" 535 | LEVEL = "smallint" 536 | IUC = "integer" 537 | GRADE = "smallint" 538 | EXP = "smallint" 539 | CHUC = "smallint" 540 | OPTION_1 = "smallint" 541 | OPTION_2 = "smallint" 542 | OPTION_3 = "smallint" 543 | SOCKET_1 = "smallint" 544 | SOCKET_2 = "smallint" 545 | LISN = "bigint" 546 | CUC = "smallint" 547 | RUC = "smallint" 548 | 549 | __primary_key__ = "inventory_item_id" 550 | __foreign_keys__ = {"INVENTORY_ITEM_ID": "inventory_items.inventory_item_id"} 551 | 552 | 553 | class InventoryItems(Table): 554 | INVENTORY_ITEM_ID = "bigint" 555 | CHARACTER_ID = "integer" 556 | STORAGE_ID = "integer" 557 | ITEM_ID = "integer" 558 | INVENTORY_TYPE = "smallint" 559 | POSITION = "integer" 560 | OWNER = "text" 561 | PET_ID = "bigint" 562 | QUANTITY = "integer" 563 | CISN = "bigint" # Current inventory Serial 564 | SN = "bigint" # Serial Number (Would replace inventory_item_id?) 565 | EXPIRE = "bigint" 566 | 567 | __primary_key__ = "inventory_item_id" 568 | 569 | 570 | # class InventoryPets(Table): 571 | # pass 572 | 573 | # class Keymap(Table): 574 | # pass 575 | 576 | # class Skills(Table): 577 | # pass 578 | 579 | 580 | class Maplestory(Schema): 581 | ACCOUNTS = Account 582 | BUDDIES = Buddies 583 | CHARACTERS = Character 584 | INVENTORY_EQUIPMENT = InventoryEquipment 585 | INVENTORY_ITEMS = InventoryItems 586 | # INVENTORY_PETS = InventoryPets 587 | # KEYMAP = Keymap 588 | # SKILLS = Skills 589 | -------------------------------------------------------------------------------- /mapy/database/db_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | from abc import ABCMeta 3 | from asyncio import get_running_loop, wait_for 4 | from datetime import date 5 | from typing import Union 6 | 7 | from asyncpg import PostgresError, create_pool 8 | from asyncpg.exceptions import InterfaceError 9 | 10 | from ..constants import ItemType 11 | from ..game import Account as Account_ 12 | from ..game import CharacterEntry 13 | from ..game import Field as _Field 14 | from ..game import ( 15 | Foothold, 16 | FuncKey, 17 | ItemSlotBundle, 18 | ItemSlotEquip, 19 | MapleCharacter, 20 | Mob, 21 | Npc, 22 | Portal, 23 | SkillEntry, 24 | SkillLevelData, 25 | ) 26 | from ..logger import Logger 27 | from ..tools import get 28 | from .schema import ( 29 | Column, 30 | Insert, 31 | IntColumn, 32 | ListArguments, 33 | Query, 34 | Schema, 35 | StringColumn, 36 | Table, 37 | Update, 38 | ) 39 | from .structure import RMDB, Maplestory 40 | 41 | 42 | def match_item(inventory_type): 43 | match inventory_type: 44 | case 1: 45 | return ItemSlotEquip 46 | case _: 47 | return ItemSlotBundle 48 | 49 | 50 | logger = Logger("DB Client") 51 | 52 | 53 | class SchemaError(PostgresError): 54 | pass 55 | 56 | 57 | class ResponseError(PostgresError): 58 | pass 59 | 60 | 61 | class QueryError(PostgresError): 62 | pass 63 | 64 | 65 | async def init_conn(conn): 66 | await conn.set_type_codec( 67 | "jsonb", encoder=json.dumps, decoder=json.loads, schema="pg_catalog" 68 | ) 69 | await conn.set_type_codec( 70 | "json", encoder=json.dumps, decoder=json.loads, schema="pg_catalog" 71 | ) 72 | 73 | 74 | DATABASE = "maplestory" 75 | ACCOUNTS = f"{DATABASE}.accounts" 76 | CHARACTERS = f"{DATABASE}.characters" 77 | ITEMS = f"{DATABASE}.inventory_items" 78 | EQUIPS = f"{DATABASE}.inventory_equipment" 79 | SKILLS = f"{DATABASE}.skills" 80 | 81 | 82 | class DatabaseClient: 83 | _name = "Database Client" 84 | 85 | def __init__( 86 | self, 87 | user="postgres", 88 | password="", 89 | host="127.0.0.1", 90 | port=5432, 91 | database="postgres", 92 | ): 93 | self._logger: Logger = Logger(self) 94 | self._user = user 95 | self._pass = password 96 | self._host = host 97 | self._port = port 98 | self._database = database 99 | self.pool = None 100 | self._dsn = f"postgres://{user}:{password}@{host}:{port}/{database}" 101 | 102 | # WZ Data 103 | self._items = Items(self) 104 | self._skills = Skills(self) 105 | 106 | @property 107 | def dsn(self): 108 | return self._dsn 109 | 110 | def log(self, message, level=None): 111 | if not self._logger: 112 | return 113 | 114 | if not level: 115 | return self._logger.log_basic(message) 116 | else: 117 | return self._logger.log(str(level).upper(), message) 118 | 119 | async def start(self): 120 | # try: 121 | self.pool = await create_pool( 122 | self.dsn, maximum_inactive_connection_lifetime=120, size=20, init=init_conn 123 | ) 124 | self.log( 125 | f"PostgreSQL Client connected at {self._host}:" 126 | f"{self._port}" 127 | ) 128 | # except Exception: 129 | # await Maplestory.create() 130 | 131 | async def stop(self): 132 | if self.pool: 133 | if not self.pool._closed: 134 | await wait_for(self.pool.close(), 5.0) 135 | 136 | self.pool.terminate() 137 | 138 | self.log("Closed PostgreSQL pool") 139 | 140 | async def recreate_pool(self): 141 | self.log("Re-Creating PostgreSQL pool", "WARNING") 142 | self.pool = await create_pool(self.dsn, loop=get_running_loop(), init=init_conn) 143 | 144 | async def initialize_database(self): 145 | pass 146 | 147 | async def execute_query(self, query, *args): 148 | result = [] 149 | 150 | if not self.pool: 151 | return None 152 | 153 | async with self.pool.acquire() as conn: 154 | stmt = await conn.prepare(query) 155 | records = await stmt.fetch(*args) 156 | 157 | for record in records: 158 | result.append(record) 159 | 160 | return result 161 | 162 | async def execute_transaction(self, query, *query_args): 163 | result = [] 164 | try: 165 | if not self.pool: 166 | return None 167 | 168 | async with self.pool.acquire() as conn: 169 | stmt = await conn.prepare(query) 170 | 171 | if any(isinstance(x, (set, tuple)) for x in query_args): 172 | async with conn.transaction(): 173 | for query_arg in query_args: 174 | async for rcrd in stmt.cursor(*query_arg): 175 | result.append(rcrd) 176 | 177 | else: 178 | async with conn.transaction(): 179 | async for rcrd in stmt.cursor(*query_args): 180 | result.append(rcrd) 181 | 182 | return result 183 | 184 | except InterfaceError: 185 | await self.recreate_pool() 186 | return await self.execute_transaction(query, *query_args) 187 | 188 | async def create_table( 189 | self, name: str, columns: list[Union[Column, str]], *, primaries=None 190 | ) -> Table: 191 | return await Table(self, name).create(columns, primaries=primaries) 192 | 193 | def table(self, name, *, schema: Union[str, Schema] | None = None) -> Table: 194 | return Table(name, self) 195 | 196 | def query(self, *tables): 197 | return Query(self, *tables) 198 | 199 | def insert(self, table): 200 | return Insert(self, table) 201 | 202 | def update(self, table): 203 | return Update(self, table) 204 | 205 | def schema(self, name): 206 | return Schema(self, name) 207 | 208 | # def account(self, *multi_acc, **single_acc): 209 | def account(self, **kwargs): 210 | """Request either an tuple of dictionary searches 211 | # or a single account search using kwargs""" 212 | # if multi_acc and single_acc: 213 | # raise ValueError("If using 'multi_acc' do not use 'single_acc'") 214 | return Account(self, **kwargs) 215 | 216 | def _accounts(self, *lookups): 217 | where_queries = {} 218 | for lookup in lookups: 219 | for k, v in lookup.items(): 220 | where_queries.setdefault(k, []) 221 | where_queries[k].append(v) 222 | 223 | @property 224 | def characters(self): 225 | return Characters(self) 226 | 227 | @property 228 | def field(self): 229 | return Field(self) 230 | 231 | @property 232 | def items(self): 233 | return self._items 234 | 235 | @property 236 | def skills(self): 237 | return self._skills 238 | 239 | 240 | def get_acc(fn): 241 | def wrapper(self, **kwargs): 242 | async def wrapped(): 243 | await self.get_account() 244 | return await fn(self) 245 | 246 | return wrapped() 247 | 248 | return wrapper 249 | 250 | 251 | def get_chars(fn): 252 | def wrapper(self, **kwargs): 253 | async def wrapped(): 254 | await self.get_account() 255 | return await fn(self, **kwargs) 256 | 257 | return wrapped() 258 | 259 | return wrapper 260 | 261 | 262 | class QueryTable(metaclass=ABCMeta): 263 | def __init__(self): 264 | self._db = None 265 | self._table = "" 266 | 267 | @property 268 | def table(self): 269 | if not self._db: 270 | raise NotImplementedError 271 | 272 | return self._db.table(self._table) 273 | 274 | def query(self, table=None): 275 | if not self._table or not self._db: 276 | raise NotImplementedError 277 | return self._db.query(table if table else self._table) 278 | 279 | # @property 280 | # def query(self): 281 | # return self._db.query(self._table) 282 | 283 | def insert(self, *args, **kwargs): 284 | return self.query.insert(*args, **kwargs) 285 | 286 | 287 | class Account(QueryTable): 288 | def __init__( 289 | self, 290 | db, 291 | *, 292 | id_=None, 293 | username=None, 294 | password=None, 295 | creation=None, 296 | last_login=None, 297 | last_ip=None, 298 | ban=False, 299 | admin=False, 300 | gender=None, 301 | **kwargs, 302 | ): 303 | 304 | self._db = db 305 | self._table = ACCOUNTS 306 | self.account = None 307 | 308 | self.id = id_ if id_ else kwargs.get("id", None) 309 | self.username = username 310 | self.password = password 311 | self.creation = creation 312 | self.last_login = last_login 313 | self.last_ip = last_ip 314 | self.ban = ban 315 | self.admin = admin 316 | self.gender = gender 317 | 318 | self.characters = Characters(self._db, account_id=self.id) 319 | 320 | async def get_account(self): 321 | # Called as a wrapper for all account functions 322 | # Loading (if exists) the account onto the client object 323 | # Needs to be lazily loaded and not hot loaded every call 324 | if not self.username or self.id: 325 | logger.info("Missing username or password to search by") 326 | 327 | search = { 328 | a: getattr(self, a) 329 | for a in ["id", "username", "password"] 330 | if hasattr(self, a) and getattr(self, a, None) 331 | } 332 | 333 | acc = await self.query().where(**search).get_first() 334 | self.account = Account_(**acc) 335 | 336 | @get_acc 337 | async def register(self): 338 | if self.account: 339 | return False 340 | 341 | if ret_id := await ( 342 | self.insert( 343 | username=self.username, 344 | password=self.password, 345 | creation=date.today(), 346 | last_ip=self.last_ip, 347 | ) 348 | .returning("accounts.id") 349 | .commit() 350 | .get_first() 351 | ): 352 | self.id = ret_id 353 | return True 354 | 355 | return False 356 | 357 | @get_acc 358 | async def login(self): 359 | if self.account is None: 360 | return (5, None) 361 | 362 | if self.account.password != self.password: 363 | return (4, None) 364 | 365 | return (0, self.account) 366 | 367 | async def get_entries(self, world_id=None): 368 | return await self.characters.get_entries(world_id=world_id) 369 | 370 | async def create_character(self, character): 371 | return await self.characters.create(character) 372 | 373 | 374 | class Characters(QueryTable): 375 | def __init__(self, db, account_id=None): 376 | self.account_id = account_id 377 | self._db = db 378 | self._table = CHARACTERS 379 | self._inventories = Inventories(db) 380 | self._func_keys = FuncKeys(db) 381 | self._skills = Skills(db) 382 | 383 | async def get_entries(self, world_id=None): 384 | entries = [] 385 | 386 | if not world_id: 387 | characters = await ( 388 | self.query().where(account_id=self.account_id).order_by("id").get() 389 | ) 390 | else: 391 | characters = await ( 392 | self.query() 393 | .where(account_id=self.account_id, world_id=world_id) 394 | .order_by("id") 395 | .get() 396 | ) 397 | 398 | for character in characters: 399 | character = CharacterEntry(**character) 400 | 401 | equips = await ( 402 | self.query(ITEMS) 403 | .where(IntColumn("position") < 0, character_id=character.id) 404 | .get() 405 | ) 406 | 407 | for item in equips: 408 | slot = item["position"] 409 | item = ItemSlotEquip(**item) 410 | character.equip.add(item, slot) 411 | 412 | entries.append(character) 413 | 414 | return entries 415 | 416 | async def load(self, character_id, client): 417 | character = await self.query().where(id=character_id).get_first() 418 | 419 | character = MapleCharacter(character) 420 | character.client = client 421 | character.data = self 422 | 423 | await self._inventories.load(character) 424 | await self._func_keys.load(character) 425 | 426 | skills = await self.query(SKILLS).where(character_id=character.id).get() 427 | 428 | # Get static data from rmdb for character skills 429 | for skill in skills: 430 | level_data = await self._db.skills.get_skill_level_data(skill["skill_id"]) 431 | character.skills[skill["skill_id"]] = SkillEntry( 432 | level_data=level_data, **skill 433 | ) 434 | 435 | return character 436 | 437 | async def get(self, **search_by): 438 | character = await self.query().where(**search_by).get_first() 439 | return character 440 | 441 | async def create(self, character): 442 | character_data = {**character.stats.__dict__} 443 | character_data["account_id"] = self.account_id 444 | character_data.pop("id") 445 | inventories = character_data.pop("inventories") 446 | 447 | character_id = ( 448 | await self.insert(**character_data) 449 | .primaries("name") 450 | .returning("characters.id") 451 | .commit() 452 | ) 453 | 454 | if character_id: 455 | character_id = character_id[0]["id"] 456 | await self.update_inventory(character_id, inventories) 457 | 458 | return character_id 459 | 460 | async def update_inventory(self, character_id, inventories): 461 | items_columns = await self._db.table( 462 | "maplestory.inventory_items" 463 | ).columns.get_names() 464 | 465 | equips_columns = await self._db.table( 466 | "maplestory.inventory_equipment" 467 | ).columns.get_names() 468 | 469 | items = inventories.get_update() 470 | 471 | for item in items: 472 | item_data = dict() 473 | for column_name in items_columns: 474 | value = item.get(column_name) 475 | if value is None: 476 | continue 477 | 478 | item_data[column_name] = value 479 | 480 | item_data["character_id"] = character_id 481 | 482 | inv_item_id = ( 483 | await self._db.table("maplestory.inventory_items") 484 | .insert(**item_data) 485 | .primaries("inventory_item_id") 486 | .returning("inventory_items.inventory_item_id") 487 | .commit(do_update=True) 488 | ) 489 | inv_item_id = inv_item_id[0]["inventory_item_id"] 490 | 491 | if item["inventory_type"] == 1: 492 | equip_data = dict() 493 | for column_name in equips_columns: 494 | value = item.get(column_name) 495 | if value is None: 496 | continue 497 | 498 | equip_data[column_name] = item.get(column_name) 499 | 500 | equip_data["inventory_item_id"] = inv_item_id 501 | 502 | await self._db.table("maplestory.inventory_equipment").insert( 503 | **equip_data 504 | ).primaries("inventory_item_id").commit(do_update=True) 505 | 506 | 507 | class InventoryItems(QueryTable): 508 | def __init__(self, db): 509 | self._db = db 510 | self._table = ITEMS 511 | 512 | 513 | class InventoryEquips(QueryTable): 514 | def __init__(self, db): 515 | self._db = db 516 | self._table = EQUIPS 517 | 518 | 519 | class Inventories: 520 | def __init__(self, db): 521 | self._db = db 522 | self.items_table = InventoryItems(db) 523 | self.equips_table = InventoryEquips(db) 524 | 525 | async def load(self, character): 526 | items = self.items_table.query().where(character_id=character.id) 527 | 528 | items_col = IntColumn("items.inventory_item_id") 529 | equips_col = IntColumn("inventory_equipment.inventory_item_id") 530 | 531 | equips = ( 532 | self._db.query(EQUIPS, "items") 533 | .select("inventory_equipment.*") 534 | .where(equips_col.in_(items_col)) 535 | ) 536 | 537 | inventory = await ( 538 | self._db.query() 539 | .with_(("items", items), ("equips", equips)) 540 | .table("items") 541 | .left_join("equips", "inventory_item_id") 542 | .get() 543 | ) 544 | 545 | for item in inventory: 546 | inventory_type = item["inventory_type"] 547 | slot = item["position"] 548 | item = match_item(inventory_type)(**item) 549 | character.inventories.add(item, slot) 550 | 551 | character.inventories.tracker.copy(*character.inventories) 552 | 553 | async def save(self, character): 554 | items = [] 555 | equips = [] 556 | 557 | for item in character.inventories.inventory_changes: 558 | item_data = {} 559 | equip_data = {} 560 | for column_name in Maplestory.INVENTORY_ITEMS.columns: 561 | value = item.get(column_name) 562 | if value is None: 563 | continue 564 | 565 | item_data[column_name] = value 566 | 567 | item_data["character_id"] = character.id 568 | 569 | items.append(item_data) 570 | 571 | if item["inventory_type"] == 1: 572 | for column_name in Maplestory.INVENTORY_EQUIPMENT.columns: 573 | value = item.get(column_name) 574 | if value is None: 575 | continue 576 | 577 | equip_data[column_name] = item.get(column_name) 578 | equip_data["position"] = item["position"] 579 | 580 | equips.append(equip_data) 581 | 582 | q = ( 583 | await self.items_table.insert.row(items) 584 | .primaries("inventory_item_id") 585 | .returning("inventory_items.inventory_item_id, inventory_items.position") 586 | .commit(do_update=True) 587 | ) 588 | 589 | if equips: 590 | for item in q: 591 | for equip in equips: 592 | if item["position"] == equip["position"]: 593 | equip["inventory_item_id"] = item["inventory_item_id"] 594 | equip.pop("position") 595 | 596 | await self._db.table("maplestory.inventory_equipment").insert.rows( 597 | equips 598 | ).primaries("inventory_item_id").commit(do_update=True) 599 | 600 | 601 | class FuncKeys(QueryTable): 602 | def __init__(self, db): 603 | self._db = db 604 | 605 | async def load(self, character): 606 | func_keys = ( 607 | await self._db.table("maplestory.keymap") 608 | .query() 609 | .select("key", "type", "action") 610 | .where(character_id=character.id) 611 | .get() 612 | ) 613 | 614 | for key in func_keys: 615 | character.func_keys[key["key"]] = FuncKey(key["type"], key["action"]) 616 | 617 | 618 | class Items: 619 | def __init__(self, db): 620 | self._db = db 621 | self._cached_items = [] 622 | self._item_data_table = self._db.table("rmdb.item_data") 623 | 624 | @property 625 | def item_data(self): 626 | return self._item_data_table 627 | 628 | async def get_many(self, *item_ids): 629 | item_ids = set(item_ids) 630 | 631 | to_cache = set( 632 | filter( 633 | lambda i: set([c.item_id for c in self._cached_items]) & set([i]), 634 | item_ids, 635 | ) 636 | ) 637 | pre_cached = list(filter(lambda i: item_ids & set([i]), self._cached_items)) 638 | if len(to_cache) < 1: 639 | return pre_cached 640 | 641 | def get_type(type_): 642 | return ListArguments( 643 | [id for id in to_cache if ItemType(id // 1000000) == type_] 644 | ) 645 | 646 | items = [] 647 | 648 | for k in ItemType: 649 | specific = get_type(k) 650 | if not len(specific): 651 | continue 652 | 653 | query = self._db.query("rmdb.item_data").select(*RMDB.ITEM_DATA.columns) 654 | 655 | # if v is not None: 656 | # query.inner_join(f"rmdb.item_{v}_data", "item_id").where( 657 | # IntColumn("item_id") in specific 658 | # ) 659 | 660 | fetched_items = await query.get() 661 | 662 | for fetched_item in fetched_items: 663 | cleaned_item = {} 664 | for k, v in fetched_item.items(): 665 | if v: 666 | cleaned_item[k] = v 667 | 668 | item = match_item(k)(**cleaned_item) 669 | items.append(item) 670 | self._cached_items.append(item) 671 | 672 | items.extend(pre_cached) 673 | return items 674 | 675 | async def get(self, item_id): 676 | pre_cached = get(self._cached_items, item_id=item_id) 677 | if pre_cached: 678 | return pre_cached 679 | 680 | item_type = item_id // 1000000 681 | 682 | query = await (self._db.query("rmdb.item_data")) 683 | 684 | if item_type == 1: 685 | query.inner_join("rmdb.item_equip_data", "item_id").where(item_id=item_id) 686 | 687 | fetched_item = await query.get_first() 688 | cleaned_item = {} 689 | 690 | for key, value in fetched_item.items(): 691 | if value is not None: 692 | cleaned_item[key] = value 693 | 694 | item = match_item(item_type)(**cleaned_item) 695 | self._cached_items.append(item) 696 | return item 697 | 698 | 699 | class Skills: 700 | def __init__(self, db): 701 | self._db = db 702 | self._cached_skills = {} 703 | 704 | async def get_skill_level_data(self, skill_id): 705 | skill_level_data = self._cached_skills.get(skill_id, None) 706 | 707 | if skill_level_data: 708 | return skill_level_data 709 | 710 | level_data = ( 711 | await self._db.table("rmdb.player_skill_data") 712 | .query() 713 | .where(skill_id=skill_id) 714 | .get_first() 715 | ) 716 | 717 | skill_level_data = SkillLevelData(**level_data) 718 | self._cached_skills[skill_id] = skill_level_data 719 | 720 | return skill_level_data 721 | 722 | 723 | class Field: 724 | def __init__(self, db): 725 | self._db = db 726 | 727 | async def get(self, map_id): 728 | _field = _Field(map_id) 729 | 730 | portals = ( 731 | await self._db.table("rmdb.map_portals").query().where(map_id=map_id).get() 732 | ) 733 | 734 | for portal in portals: 735 | _field.portals.add(Portal(**portal)) 736 | 737 | footholds = ( 738 | await self._db.table("rmdb.map_footholds") 739 | .query() 740 | .where(map_id=map_id) 741 | .get() 742 | ) 743 | 744 | for foothold in footholds: 745 | _field.footholds.add(Foothold(**foothold)) 746 | 747 | life = self._db.query("rmdb.map_life").where(map_id=map_id) 748 | 749 | life_column = IntColumn("life_id") 750 | mob_column = IntColumn("mob_id") 751 | 752 | mobs = ( 753 | self._db.query("rmdb.mob_data", "life") 754 | .select("mob_data.*", "life.life_type", distinct=True) 755 | .where(StringColumn("life.life_type") == "mob", mob_column.in_(life_column)) 756 | ) 757 | 758 | all_life = ( 759 | self._db.query() 760 | .with_(("life", life), ("mobs", mobs)) 761 | .table("life") 762 | .left_join("mobs", "life_type", "flags") 763 | ) 764 | 765 | all_life = await all_life.get() 766 | 767 | for life_obj in all_life: 768 | if life_obj["life_type"] == "mob": 769 | _field.mobs.add(Mob(**life_obj)) 770 | elif life_obj["life_type"] == "npc": 771 | _field.npcs.add(Npc(**life_obj)) 772 | 773 | return _field 774 | -------------------------------------------------------------------------------- /mapy/opcodes.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class OpCode(IntEnum): 5 | ... 6 | 7 | 8 | class CRecvOps(OpCode): 9 | CP_CheckPassword = 0x1 10 | CP_GuestIDLogin = 0x2 11 | CP_AccountInfoRequest = 0x3 12 | CP_WorldInfoRequest = 0x4 13 | CP_SelectWorld = 0x5 14 | CP_CheckUserLimit = 0x6 15 | CP_ConfirmEULA = 0x7 16 | CP_SetGender = 0x8 17 | CP_CheckPinCode = 0x9 18 | CP_UpdatePinCode = 0xA 19 | CP_WorldRequest = 0xB 20 | CP_LogoutWorld = 0xC 21 | CP_ViewAllChar = 0xD 22 | CP_SelectCharacterByVAC = 0xE 23 | CP_VACFlagSet = 0xF 24 | CP_CheckNameChangePossible = 0x10 25 | CP_RegisterNewCharacter = 0x11 26 | CP_CheckTransferWorldPossible = 0x12 27 | CP_SelectCharacter = 0x13 28 | CP_MigrateIn = 0x14 29 | CP_CheckDuplicatedID = 0x15 30 | CP_CreateNewCharacter = 0x16 31 | CP_CreateNewCharacterInCS = 0x17 32 | CP_DeleteCharacter = 0x18 33 | CP_AliveAck = 0x19 34 | CP_ExceptionLog = 0x1A 35 | CP_SecurityPacket = 0x1B 36 | CP_EnableSPWRequest = 0x1C 37 | CP_CheckSPWRequest = 0x1D 38 | CP_EnableSPWRequestByACV = 0x1E 39 | CP_CheckSPWRequestByACV = 0x1F 40 | CP_CheckOTPRequest = 0x20 41 | CP_CheckDeleteCharacterOTP = 0x21 42 | CP_CreateSecurityHandle = 0x22 43 | CP_SSOErrorLog = 0x23 44 | CP_ClientDumpLog = 0x24 45 | CP_CheckExtraCharInfo = 0x25 46 | CP_CreateNewCharacter_Ex = 0x26 47 | CP_END_SOCKET = 0x27 48 | CP_BEGIN_USER = 0x28 49 | CP_UserTransferFieldRequest = 0x29 50 | CP_UserTransferChannelRequest = 0x2A 51 | CP_UserMigrateToCashShopRequest = 0x2B 52 | CP_UserMove = 0x2C 53 | CP_UserSitRequest = 0x2D 54 | CP_UserPortableChairSitRequest = 0x2E 55 | CP_UserMeleeAttack = 0x2F 56 | CP_UserShootAttack = 0x30 57 | CP_UserMagicAttack = 0x31 58 | CP_UserBodyAttack = 0x32 59 | CP_UserMovingShootAttackPrepare = 0x33 60 | CP_UserHit = 0x34 61 | CP_UserAttackUser = 0x35 62 | CP_UserChat = 0x36 63 | CP_UserADBoardClose = 0x37 64 | CP_UserEmotion = 0x38 65 | CP_UserActivateEffectItem = 0x39 66 | CP_UserUpgradeTombEffect = 0x3A 67 | CP_UserHP = 0x3B 68 | CP_Premium = 0x3C 69 | CP_UserBanMapByMob = 0x3D 70 | CP_UserMonsterBookSetCover = 0x3E 71 | CP_UserSelectNpc = 0x3F 72 | CP_UserRemoteShopOpenRequest = 0x40 73 | CP_UserScriptMessageAnswer = 0x41 74 | CP_UserShopRequest = 0x42 75 | CP_UserTrunkRequest = 0x43 76 | CP_UserEntrustedShopRequest = 0x44 77 | CP_UserStoreBankRequest = 0x45 78 | CP_UserParcelRequest = 0x46 79 | CP_UserEffectLocal = 0x47 80 | CP_ShopScannerRequest = 0x48 81 | CP_ShopLinkRequest = 0x49 82 | CP_AdminShopRequest = 0x4A 83 | CP_UserGatherItemRequest = 0x4B 84 | CP_UserSortItemRequest = 0x4C 85 | CP_UserChangeSlotPositionRequest = 0x4D 86 | CP_UserStatChangeItemUseRequest = 0x4E 87 | CP_UserStatChangeItemCancelRequest = 0x4F 88 | CP_UserStatChangeByPortableChairRequest = 0x50 89 | CP_UserMobSummonItemUseRequest = 0x51 90 | CP_UserPetFoodItemUseRequest = 0x52 91 | CP_UserTamingMobFoodItemUseRequest = 0x53 92 | CP_UserScriptItemUseRequest = 0x54 93 | CP_UserConsumeCashItemUseRequest = 0x55 94 | CP_UserDestroyPetItemRequest = 0x56 95 | CP_UserBridleItemUseRequest = 0x57 96 | CP_UserSkillLearnItemUseRequest = 0x58 97 | CP_UserSkillResetItemUseRequest = 0x59 98 | CP_UserShopScannerItemUseRequest = 0x5A 99 | CP_UserMapTransferItemUseRequest = 0x5B 100 | CP_UserPortalScrollUseRequest = 0x5C 101 | CP_UserUpgradeItemUseRequest = 0x5D 102 | CP_UserHyperUpgradeItemUseRequest = 0x5E 103 | CP_UserItemOptionUpgradeItemUseRequest = 0x5F 104 | CP_UserUIOpenItemUseRequest = 0x60 105 | CP_UserItemReleaseRequest = 0x61 106 | CP_UserAbilityUpRequest = 0x62 107 | CP_UserAbilityMassUpRequest = 0x63 108 | CP_UserChangeStatRequest = 0x64 109 | CP_UserChangeStatRequestByItemOption = 0x65 110 | CP_UserSkillUpRequest = 0x66 111 | CP_UserSkillUseRequest = 0x67 112 | CP_UserSkillCancelRequest = 0x68 113 | CP_UserSkillPrepareRequest = 0x69 114 | CP_UserDropMoneyRequest = 0x6A 115 | CP_UserGivePopularityRequest = 0x6B 116 | CP_UserPartyRequest = 0x6C 117 | CP_UserCharacterInfoRequest = 0x6D 118 | CP_UserActivatePetRequest = 0x6E 119 | CP_UserTemporaryStatUpdateRequest = 0x6F 120 | CP_UserPortalScriptRequest = 0x70 121 | CP_UserPortalTeleportRequest = 0x71 122 | CP_UserMapTransferRequest = 0x72 123 | CP_UserAntiMacroItemUseRequest = 0x73 124 | CP_UserAntiMacroSkillUseRequest = 0x74 125 | CP_UserAntiMacroQuestionResult = 0x75 126 | CP_UserClaimRequest = 0x76 127 | CP_UserQuestRequest = 0x77 128 | CP_UserCalcDamageStatSetRequest = 0x78 129 | CP_UserThrowGrenade = 0x79 130 | CP_UserMacroSysDataModified = 0x7A 131 | CP_UserSelectNpcItemUseRequest = 0x7B 132 | CP_UserLotteryItemUseRequest = 0x7C 133 | CP_UserItemMakeRequest = 0x7D 134 | CP_UserSueCharacterRequest = 0x7E 135 | CP_UserUseGachaponBoxRequest = 0x7F 136 | CP_UserUseGachaponRemoteRequest = 0x80 137 | CP_UserUseWaterOfLife = 0x81 138 | CP_UserRepairDurabilityAll = 0x82 139 | CP_UserRepairDurability = 0x83 140 | CP_UserQuestRecordSetState = 0x84 141 | CP_UserClientTimerEndRequest = 0x85 142 | CP_UserFollowCharacterRequest = 0x86 143 | CP_UserFollowCharacterWithdraw = 0x87 144 | CP_UserSelectPQReward = 0x88 145 | CP_UserRequestPQReward = 0x89 146 | CP_SetPassenserResult = 0x8A 147 | CP_BroadcastMsg = 0x8B 148 | CP_GroupMessage = 0x8C 149 | CP_Whisper = 0x8D 150 | CP_CoupleMessage = 0x8E 151 | CP_Messenger = 0x8F 152 | CP_MiniRoom = 0x90 153 | CP_PartyRequest = 0x91 154 | CP_PartyResult = 0x92 155 | CP_ExpeditionRequest = 0x93 156 | CP_PartyAdverRequest = 0x94 157 | CP_GuildRequest = 0x95 158 | CP_GuildResult = 0x96 159 | CP_Admin = 0x97 160 | CP_Log = 0x98 161 | CP_FriendRequest = 0x99 162 | CP_MemoRequest = 0x9A 163 | CP_MemoFlagRequest = 0x9B 164 | CP_EnterTownPortalRequest = 0x9C 165 | CP_EnterOpenGateRequest = 0x9D 166 | CP_SlideRequest = 0x9E 167 | CP_FuncKeyMappedModified = 0x9F 168 | CP_RPSGame = 0xA0 169 | CP_MarriageRequest = 0xA1 170 | CP_WeddingWishListRequest = 0xA2 171 | CP_WeddingProgress = 0xA3 172 | CP_GuestBless = 0xA4 173 | CP_BoobyTrapAlert = 0xA5 174 | CP_StalkBegin = 0xA6 175 | CP_AllianceRequest = 0xA7 176 | CP_AllianceResult = 0xA8 177 | CP_FamilyChartRequest = 0xA9 178 | CP_FamilyInfoRequest = 0xAA 179 | CP_FamilyRegisterJunior = 0xAB 180 | CP_FamilyUnregisterJunior = 0xAC 181 | CP_FamilyUnregisterParent = 0xAD 182 | CP_FamilyJoinResult = 0xAE 183 | CP_FamilyUsePrivilege = 0xAF 184 | CP_FamilySetPrecept = 0xB0 185 | CP_FamilySummonResult = 0xB1 186 | CP_ChatBlockUserReq = 0xB2 187 | CP_GuildBBS = 0xB3 188 | CP_UserMigrateToITCRequest = 0xB4 189 | CP_UserExpUpItemUseRequest = 0xB5 190 | CP_UserTempExpUseRequest = 0xB6 191 | CP_NewYearCardRequest = 0xB7 192 | CP_RandomMorphRequest = 0xB8 193 | CP_CashItemGachaponRequest = 0xB9 194 | CP_CashGachaponOpenRequest = 0xBA 195 | CP_ChangeMaplePointRequest = 0xBB 196 | CP_TalkToTutor = 0xBC 197 | CP_RequestIncCombo = 0xBD 198 | CP_MobCrcKeyChangedReply = 0xBE 199 | CP_RequestSessionValue = 0xBF 200 | CP_UpdateGMBoard = 0xC0 201 | CP_AccountMoreInfo = 0xC1 202 | CP_FindFriend = 0xC2 203 | CP_AcceptAPSPEvent = 0xC3 204 | CP_UserDragonBallBoxRequest = 0xC4 205 | CP_UserDragonBallSummonRequest = 0xC5 206 | CP_BEGIN_PET = 0xC6 207 | CP_PetMove = 0xC7 208 | CP_PetAction = 0xC8 209 | CP_PetInteractionRequest = 0xC9 210 | CP_PetDropPickUpRequest = 0xCA 211 | CP_PetStatChangeItemUseRequest = 0xCB 212 | CP_PetUpdateExceptionListRequest = 0xCC 213 | CP_END_PET = 0xCD 214 | CP_BEGIN_SUMMONED = 0xCE 215 | CP_SummonedMove = 0xCF 216 | CP_SummonedAttack = 0xD0 217 | CP_SummonedHit = 0xD1 218 | CP_SummonedSkill = 0xD2 219 | CP_Remove = 0xD3 220 | CP_END_SUMMONED = 0xD4 221 | CP_BEGIN_DRAGON = 0xD5 222 | CP_DragonMove = 0xD6 223 | CP_END_DRAGON = 0xD7 224 | CP_QuickslotKeyMappedModified = 0xD8 225 | CP_PassiveskillInfoUpdate = 0xD9 226 | CP_UpdateScreenSetting = 0xDA 227 | CP_UserAttackUser_Specific = 0xDB 228 | CP_UserPamsSongUseRequest = 0xDC 229 | CP_QuestGuideRequest = 0xDD 230 | CP_UserRepeatEffectRemove = 0xDE 231 | CP_END_USER = 0xDF 232 | CP_BEGIN_FIELD = 0xE0 233 | CP_BEGIN_LIFEPOOL = 0xE1 234 | CP_BEGIN_MOB = 0xE2 235 | CP_MobMove = 0xE3 236 | CP_MobApplyCtrl = 0xE4 237 | CP_MobDropPickUpRequest = 0xE5 238 | CP_MobHitByObstacle = 0xE6 239 | CP_MobHitByMob = 0xE7 240 | CP_MobSelfDestruct = 0xE8 241 | CP_MobAttackMob = 0xE9 242 | CP_MobSkillDelayEnd = 0xEA 243 | CP_MobTimeBombEnd = 0xEB 244 | CP_MobEscortCollision = 0xEC 245 | CP_MobRequestEscortInfo = 0xED 246 | CP_MobEscortStopEndRequest = 0xEE 247 | CP_END_MOB = 0xEF 248 | CP_BEGIN_NPC = 0xF0 249 | CP_NpcMove = 0xF1 250 | CP_NpcSpecialAction = 0xF2 251 | CP_END_NPC = 0xF3 252 | CP_END_LIFEPOOL = 0xF4 253 | CP_BEGIN_DROPPOOL = 0xF5 254 | CP_DropPickUpRequest = 0xF6 255 | CP_END_DROPPOOL = 0xF7 256 | CP_BEGIN_REACTORPOOL = 0xF8 257 | CP_ReactorHit = 0xF9 258 | CP_ReactorTouch = 0xFA 259 | CP_RequireFieldObstacleStatus = 0xFB 260 | CP_END_REACTORPOOL = 0xFC 261 | CP_BEGIN_EVENT_FIELD = 0xFD 262 | CP_EventStart = 0xFE 263 | CP_SnowBallHit = 0xFF 264 | CP_SnowBallTouch = 0x100 265 | CP_CoconutHit = 0x101 266 | CP_TournamentMatchTable = 0x102 267 | CP_PulleyHit = 0x103 268 | CP_END_EVENT_FIELD = 0x104 269 | CP_BEGIN_MONSTER_CARNIVAL_FIELD = 0x105 270 | CP_MCarnivalRequest = 0x106 271 | CP_END_MONSTER_CARNIVAL_FIELD = 0x107 272 | CP_CONTISTATE = 0x108 273 | CP_BEGIN_PARTY_MATCH = 0x109 274 | CP_INVITE_PARTY_MATCH = 0x10A 275 | CP_CANCEL_INVITE_PARTY_MATCH = 0x10B 276 | CP_END_PARTY_MATCH = 0x10C 277 | CP_RequestFootHoldInfo = 0x10D 278 | CP_FootHoldInfo = 0x10E 279 | CP_END_FIELD = 0x10F 280 | CP_BEGIN_CASHSHOP = 0x110 281 | CP_CashShopChargeParamRequest = 0x111 282 | CP_CashShopQueryCashRequest = 0x112 283 | CP_CashShopCashItemRequest = 0x113 284 | CP_CashShopCheckCouponRequest = 0x114 285 | CP_CashShopGiftMateInfoRequest = 0x115 286 | CP_END_CASHSHOP = 0x116 287 | CP_CheckSSN2OnCreateNewCharacter = 0x117 288 | CP_CheckSPWOnCreateNewCharacter = 0x118 289 | CP_FirstSSNOnCreateNewCharacter = 0x119 290 | CP_BEGIN_RAISE = 0x11A 291 | CP_RaiseRefesh = 0x11B 292 | CP_RaiseUIState = 0x11C 293 | CP_RaiseIncExp = 0x11D 294 | CP_RaiseAddPiece = 0x11E 295 | CP_END_RAISE = 0x11F 296 | CP_SendMateMail = 0x120 297 | CP_RequestGuildBoardAuthKey = 0x121 298 | CP_RequestConsultAuthKey = 0x122 299 | CP_RequestClassCompetitionAuthKey = 0x123 300 | CP_RequestWebBoardAuthKey = 0x124 301 | CP_BEGIN_ITEMUPGRADE = 0x125 302 | CP_GoldHammerRequest = 0x126 303 | CP_GoldHammerComplete = 0x127 304 | CP_ItemUpgradeComplete = 0x128 305 | CP_END_ITEMUPGRADE = 0x129 306 | CP_BEGIN_BATTLERECORD = 0x12A 307 | CP_BATTLERECORD_ONOFF_REQUEST = 0x12B 308 | CP_END_BATTLERECORD = 0x12C 309 | CP_BEGIN_MAPLETV = 0x12D 310 | CP_MapleTVSendMessageRequest = 0x12E 311 | CP_MapleTVUpdateViewCount = 0x12F 312 | CP_END_MAPLETV = 0x130 313 | CP_BEGIN_ITC = 0x131 314 | CP_ITCChargeParamRequest = 0x132 315 | CP_ITCQueryCashRequest = 0x133 316 | CP_ITCItemRequest = 0x134 317 | CP_END_ITC = 0x135 318 | CP_BEGIN_CHARACTERSALE = 0x136 319 | CP_CheckDuplicatedIDInCS = 0x137 320 | CP_END_CHARACTERSALE = 0x138 321 | CP_LogoutGiftSelect = 0x139 322 | CP_NO = 0x13A 323 | 324 | 325 | class CSendOps(OpCode): 326 | LP_CheckPasswordResult = 0x0 327 | LP_GuestIDLoginResult = 0x1 328 | LP_AccountInfoResult = 0x2 329 | LP_CheckUserLimitResult = 0x3 330 | LP_SetAccountResult = 0x4 331 | LP_ConfirmEULAResult = 0x5 332 | LP_CheckPinCodeResult = 0x6 333 | LP_UpdatePinCodeResult = 0x7 334 | LP_ViewAllCharResult = 0x8 335 | LP_SelectCharacterByVACResult = 0x9 336 | LP_WorldInformation = 0xA 337 | LP_SelectWorldResult = 0xB 338 | LP_SelectCharacterResult = 0xC 339 | LP_CheckDuplicatedIDResult = 0xD 340 | LP_CreateNewCharacterResult = 0xE 341 | LP_DeleteCharacterResult = 0xF 342 | LP_MigrateCommand = 0x10 343 | LP_AliveReq = 0x11 344 | LP_AuthenCodeChanged = 0x12 345 | LP_AuthenMessage = 0x13 346 | LP_SecurityPacket = 0x14 347 | LP_EnableSPWResult = 0x15 348 | LP_DeleteCharacterOTPRequest = 0x16 349 | LP_CheckCrcResult = 0x17 350 | LP_LatestConnectedWorld = 0x18 351 | LP_RecommendWorldMessage = 0x19 352 | LP_CheckExtraCharInfoResult = 0x1A 353 | LP_CheckSPWResult = 0x1B 354 | # LP_END_SOCKET = 0x1B 355 | # LP_BEGIN_CHARACTERDATA = 0x1C 356 | LP_InventoryOperation = 0x1C 357 | LP_InventoryGrow = 0x1D 358 | LP_StatChanged = 0x1E 359 | LP_TemporaryStatSet = 0x1F 360 | LP_TemporaryStatReset = 0x20 361 | LP_ForcedStatSet = 0x21 362 | LP_ForcedStatReset = 0x22 363 | LP_ChangeSkillRecordResult = 0x23 364 | LP_SkillUseResult = 0x24 365 | LP_GivePopularityResult = 0x25 366 | LP_Message = 0x26 367 | LP_SendOpenFullClientLink = 0x27 368 | LP_MemoResult = 0x28 369 | LP_MapTransferResult = 0x29 370 | LP_AntiMacroResult = 0x2A 371 | LP_InitialQuizStart = 0x2B 372 | LP_ClaimResult = 0x2C 373 | LP_SetClaimSvrAvailableTime = 0x2D 374 | LP_ClaimSvrStatusChanged = 0x2E 375 | LP_SetTamingMobInfo = 0x2F 376 | LP_QuestClear = 0x30 377 | LP_EntrustedShopCheckResult = 0x31 378 | LP_SkillLearnItemResult = 0x32 379 | LP_SkillResetItemResult = 0x33 380 | LP_GatherItemResult = 0x34 381 | LP_SortItemResult = 0x35 382 | LP_RemoteShopOpenResult = 0x36 383 | LP_SueCharacterResult = 0x37 384 | LP_MigrateToCashShopResult = 0x38 385 | LP_TradeMoneyLimit = 0x39 386 | LP_SetGender = 0x3A 387 | LP_GuildBBS = 0x3B 388 | LP_PetDeadMessage = 0x3C 389 | LP_CharacterInfo = 0x3D 390 | LP_PartyResult = 0x3E 391 | LP_ExpeditionRequest = 0x3F 392 | LP_ExpeditionNoti = 0x40 393 | LP_FriendResult = 0x41 394 | LP_GuildRequest = 0x42 395 | LP_GuildResult = 0x43 396 | LP_AllianceResult = 0x44 397 | LP_TownPortal = 0x45 398 | LP_OpenGate = 0x46 399 | LP_BroadcastMsg = 0x47 400 | LP_IncubatorResult = 0x48 401 | LP_ShopScannerResult = 0x49 402 | LP_ShopLinkResult = 0x4A 403 | LP_MarriageRequest = 0x4B 404 | LP_MarriageResult = 0x4C 405 | LP_WeddingGiftResult = 0x4D 406 | LP_MarriedPartnerMapTransfer = 0x4E 407 | LP_CashPetFoodResult = 0x4F 408 | LP_SetWeekEventMessage = 0x50 409 | LP_SetPotionDiscountRate = 0x51 410 | LP_BridleMobCatchFail = 0x52 411 | LP_ImitatedNPCResult = 0x53 412 | LP_ImitatedNPCData = 0x54 413 | LP_LimitedNPCDisableInfo = 0x55 414 | LP_MonsterBookSetCard = 0x56 415 | LP_MonsterBookSetCover = 0x57 416 | LP_HourChanged = 0x58 417 | LP_MiniMapOnOff = 0x59 418 | LP_ConsultAuthkeyUpdate = 0x5A 419 | LP_ClassCompetitionAuthkeyUpdate = 0x5B 420 | LP_WebBoardAuthkeyUpdate = 0x5C 421 | LP_SessionValue = 0x5D 422 | LP_PartyValue = 0x5E 423 | LP_FieldSetVariable = 0x5F 424 | LP_BonusExpRateChanged = 0x60 425 | LP_PotionDiscountRateChanged = 0x61 426 | LP_FamilyChartResult = 0x62 427 | LP_FamilyInfoResult = 0x63 428 | LP_FamilyResult = 0x64 429 | LP_FamilyJoinRequest = 0x65 430 | LP_FamilyJoinRequestResult = 0x66 431 | LP_FamilyJoinAccepted = 0x67 432 | LP_FamilyPrivilegeList = 0x68 433 | LP_FamilyFamousPointIncResult = 0x69 434 | LP_FamilyNotifyLoginOrLogout = 0x6A 435 | LP_FamilySetPrivilege = 0x6B 436 | LP_FamilySummonRequest = 0x6C 437 | LP_NotifyLevelUp = 0x6D 438 | LP_NotifyWedding = 0x6E 439 | LP_NotifyJobChange = 0x6F 440 | LP_IncRateChanged = 0x70 441 | LP_MapleTVUseRes = 0x71 442 | LP_AvatarMegaphoneRes = 0x72 443 | LP_AvatarMegaphoneUpdateMessage = 0x73 444 | LP_AvatarMegaphoneClearMessage = 0x74 445 | LP_CancelNameChangeResult = 0x75 446 | LP_CancelTransferWorldResult = 0x76 447 | LP_DestroyShopResult = 0x77 448 | LP_FAKEGMNOTICE = 0x78 449 | LP_SuccessInUseGachaponBox = 0x79 450 | LP_NewYearCardRes = 0x7A 451 | LP_RandomMorphRes = 0x7B 452 | LP_CancelNameChangeByOther = 0x7C 453 | LP_SetBuyEquipExt = 0x7D 454 | LP_SetPassenserRequest = 0x7E 455 | LP_ScriptProgressMessage = 0x7F 456 | LP_DataCRCCheckFailed = 0x80 457 | LP_CakePieEventResult = 0x81 458 | LP_UpdateGMBoard = 0x82 459 | LP_ShowSlotMessage = 0x83 460 | LP_WildHunterInfo = 0x84 461 | LP_AccountMoreInfo = 0x85 462 | LP_FindFirend = 0x86 463 | LP_StageChange = 0x87 464 | LP_DragonBallBox = 0x88 465 | LP_AskUserWhetherUsePamsSong = 0x89 466 | LP_TransferChannel = 0x8A 467 | LP_DisallowedDeliveryQuestList = 0x8B 468 | LP_MacroSysDataInit = 0x8C 469 | # LP_END_CHARACTERDATA = 0x8C 470 | # LP_BEGIN_STAGE = 0x8D 471 | LP_SetField = 0x8D 472 | LP_SetITC = 0x8E 473 | LP_SetCashShop = 0x8F 474 | # LP_END_STAGE = 0x8F 475 | # LP_BEGIN_MAP = 0x90 476 | LP_SetBackgroundEffect = 0x90 477 | LP_SetMapObjectVisible = 0x91 478 | LP_ClearBackgroundEffect = 0x92 479 | # LP_END_MAP = 0x92 480 | # LP_BEGIN_FIELD = 0x93 481 | LP_TransferFieldReqIgnored = 0x93 482 | LP_TransferChannelReqIgnored = 0x94 483 | LP_FieldSpecificData = 0x95 484 | LP_GroupMessage = 0x96 485 | LP_Whisper = 0x97 486 | LP_CoupleMessage = 0x98 487 | LP_MobSummonItemUseResult = 0x99 488 | LP_FieldEffect = 0x9A 489 | LP_FieldObstacleOnOff = 0x9B 490 | LP_FieldObstacleOnOffStatus = 0x9C 491 | LP_FieldObstacleAllReset = 0x9D 492 | LP_BlowWeather = 0x9E 493 | LP_PlayJukeBox = 0x9F 494 | LP_AdminResult = 0xA0 495 | LP_Quiz = 0xA1 496 | LP_Desc = 0xA2 497 | LP_Clock = 0xA3 498 | LP_CONTIMOVE = 0xA4 499 | LP_CONTISTATE = 0xA5 500 | LP_SetQuestClear = 0xA6 501 | LP_SetQuestTime = 0xA7 502 | LP_Warn = 0xA8 503 | LP_SetObjectState = 0xA9 504 | LP_DestroyClock = 0xAA 505 | LP_ShowArenaResult = 0xAB 506 | LP_StalkResult = 0xAC 507 | LP_MassacreIncGauge = 0xAD 508 | LP_MassacreResult = 0xAE 509 | LP_QuickslotMappedInit = 0xAF 510 | LP_FootHoldInfo = 0xB0 511 | LP_RequestFootHoldInfo = 0xB1 512 | LP_FieldKillCount = 0xB2 513 | # LP_BEGIN_USERPOOL = 0xB3 514 | LP_UserEnterField = 0xB3 515 | LP_UserLeaveField = 0xB4 516 | # LP_BEGIN_USERCOMMON = 0xB5 517 | LP_UserChat = 0xB5 518 | LP_UserChatNLCPQ = 0xB6 519 | LP_UserADBoard = 0xB7 520 | LP_UserMiniRoomBalloon = 0xB8 521 | LP_UserConsumeItemEffect = 0xB9 522 | LP_UserItemUpgradeEffect = 0xBA 523 | LP_UserItemHyperUpgradeEffect = 0xBB 524 | LP_UserItemOptionUpgradeEffect = 0xBC 525 | LP_UserItemReleaseEffect = 0xBD 526 | LP_UserItemUnreleaseEffect = 0xBE 527 | LP_UserHitByUser = 0xBF 528 | LP_UserTeslaTriangle = 0xC0 529 | LP_UserFollowCharacter = 0xC1 530 | LP_UserShowPQReward = 0xC2 531 | LP_UserSetPhase = 0xC3 532 | LP_SetPortalUsable = 0xC4 533 | LP_ShowPamsSongResult = 0xC5 534 | # LP_BEGIN_PET = 0xC6 535 | LP_PetActivated = 0xC6 536 | LP_PetEvol = 0xC7 537 | LP_PetTransferField = 0xC8 538 | LP_PetMove = 0xC9 539 | LP_PetAction = 0xCA 540 | LP_PetNameChanged = 0xCB 541 | LP_PetLoadExceptionList = 0xCC 542 | LP_PetActionCommand = 0xCD 543 | # LP_END_PET = 0xCD 544 | # LP_BEGIN_DRAGON = 0xCE 545 | LP_DragonEnterField = 0xCE 546 | LP_DragonMove = 0xCF 547 | LP_DragonLeaveField = 0xD0 548 | # LP_END_DRAGON = 0xD0 549 | LP_END_USERCOMMON = 0xD1 550 | # LP_BEGIN_USERREMOTE = 0xD2 551 | LP_UserMove = 0xD2 552 | LP_UserMeleeAttack = 0xD3 553 | LP_UserShootAttack = 0xD4 554 | LP_UserMagicAttack = 0xD5 555 | LP_UserBodyAttack = 0xD6 556 | LP_UserSkillPrepare = 0xD7 557 | LP_UserMovingShootAttackPrepare = 0xD8 558 | LP_UserSkillCancel = 0xD9 559 | LP_UserHit = 0xDA 560 | LP_UserEmotion = 0xDB 561 | LP_UserSetActiveEffectItem = 0xDC 562 | LP_UserShowUpgradeTombEffect = 0xDD 563 | LP_UserSetActivePortableChair = 0xDE 564 | LP_UserAvatarModified = 0xDF 565 | LP_UserEffectRemote = 0xE0 566 | LP_UserTemporaryStatSet = 0xE1 567 | LP_UserTemporaryStatReset = 0xE2 568 | LP_UserHP = 0xE3 569 | LP_UserGuildNameChanged = 0xE4 570 | LP_UserGuildMarkChanged = 0xE5 571 | LP_UserThrowGrenade = 0xE6 572 | # LP_END_USERREMOTE = 0xE6 573 | # LP_BEGIN_USERLOCAL = 0xE7 574 | LP_UserSitResult = 0xE7 575 | LP_UserEmotionLocal = 0xE8 576 | LP_UserEffectLocal = 0xE9 577 | LP_UserTeleport = 0xEA 578 | LP_Premium = 0xEB 579 | LP_MesoGive_Succeeded = 0xEC 580 | LP_MesoGive_Failed = 0xED 581 | LP_Random_Mesobag_Succeed = 0xEE 582 | LP_Random_Mesobag_Failed = 0xEF 583 | LP_FieldFadeInOut = 0xF0 584 | LP_FieldFadeOutForce = 0xF1 585 | LP_UserQuestResult = 0xF2 586 | LP_NotifyHPDecByField = 0xF3 587 | LP_UserPetSkillChanged = 0xF4 588 | LP_UserBalloonMsg = 0xF5 589 | LP_PlayEventSound = 0xF6 590 | LP_PlayMinigameSound = 0xF7 591 | LP_UserMakerResult = 0xF8 592 | LP_UserOpenConsultBoard = 0xF9 593 | LP_UserOpenClassCompetitionPage = 0xFA 594 | LP_UserOpenUI = 0xFB 595 | LP_UserOpenUIWithOption = 0xFC 596 | LP_SetDirectionMode = 0xFD 597 | LP_SetStandAloneMode = 0xFE 598 | LP_UserHireTutor = 0xFF 599 | LP_UserTutorMsg = 0x100 600 | LP_IncCombo = 0x101 601 | LP_UserRandomEmotion = 0x102 602 | LP_ResignQuestReturn = 0x103 603 | LP_PassMateName = 0x104 604 | LP_SetRadioSchedule = 0x105 605 | LP_UserOpenSkillGuide = 0x106 606 | LP_UserNoticeMsg = 0x107 607 | LP_UserChatMsg = 0x108 608 | LP_UserBuffzoneEffect = 0x109 609 | LP_UserGoToCommoditySN = 0x10A 610 | LP_UserDamageMeter = 0x10B 611 | LP_UserTimeBombAttack = 0x10C 612 | LP_UserPassiveMove = 0x10D 613 | LP_UserFollowCharacterFailed = 0x10E 614 | LP_UserRequestVengeance = 0x10F 615 | LP_UserRequestExJablin = 0x110 616 | LP_UserAskAPSPEvent = 0x111 617 | LP_QuestGuideResult = 0x112 618 | LP_UserDeliveryQuest = 0x113 619 | LP_SkillCooltimeSet = 0x114 620 | # LP_END_USERLOCAL = 0x114 621 | # LP_END_USERPOOL = 0x115 622 | # LP_BEGIN_SUMMONED = 0x116 623 | LP_SummonedEnterField = 0x116 624 | LP_SummonedLeaveField = 0x117 625 | LP_SummonedMove = 0x118 626 | LP_SummonedAttack = 0x119 627 | LP_SummonedSkill = 0x11A 628 | LP_SummonedHit = 0x11B 629 | # LP_END_SUMMONED = 0x11B 630 | # LP_BEGIN_MOBPOOL = 0x11C 631 | LP_MobEnterField = 0x11C 632 | LP_MobLeaveField = 0x11D 633 | LP_MobChangeController = 0x11E 634 | # LP_BEGIN_MOB = 0x11F 635 | LP_MobMove = 0x11F 636 | LP_MobCtrlAck = 0x120 637 | LP_MobCtrlHint = 0x121 638 | LP_MobStatSet = 0x122 639 | LP_MobStatReset = 0x123 640 | LP_MobSuspendReset = 0x124 641 | LP_MobAffected = 0x125 642 | LP_MobDamaged = 0x126 643 | LP_MobSpecialEffectBySkill = 0x127 644 | LP_MobHPChange = 0x128 645 | LP_MobCrcKeyChanged = 0x129 646 | LP_MobHPIndicator = 0x12A 647 | LP_MobCatchEffect = 0x12B 648 | LP_MobEffectByItem = 0x12C 649 | LP_MobSpeaking = 0x12D 650 | LP_MobChargeCount = 0x12E 651 | LP_MobSkillDelay = 0x12F 652 | LP_MobRequestResultEscortInfo = 0x130 653 | LP_MobEscortStopEndPermmision = 0x131 654 | LP_MobEscortStopSay = 0x132 655 | LP_MobEscortReturnBefore = 0x133 656 | LP_MobNextAttack = 0x134 657 | LP_MobAttackedByMob = 0x135 658 | # LP_END_MOB = 0x135 659 | LP_END_MOBPOOL = 0x136 660 | # LP_BEGIN_NPCPOOL = 0x137 661 | LP_NpcEnterField = 0x137 662 | LP_NpcLeaveField = 0x138 663 | LP_NpcChangeController = 0x139 664 | # LP_BEGIN_NPC = 0x13A 665 | LP_NpcMove = 0x13A 666 | LP_NpcUpdateLimitedInfo = 0x13B 667 | LP_NpcSpecialAction = 0x13C 668 | # LP_END_NPC = 0x13C 669 | # LP_BEGIN_NPCTEMPLATE = 0x13D 670 | LP_NpcSetScript = 0x13D 671 | # LP_END_NPCTEMPLATE = 0x13D 672 | LP_END_NPCPOOL = 0x13E 673 | # LP_BEGIN_EMPLOYEEPOOL = 0x13F 674 | LP_EmployeeEnterField = 0x13F 675 | LP_EmployeeLeaveField = 0x140 676 | LP_EmployeeMiniRoomBalloon = 0x141 677 | # LP_END_EMPLOYEEPOOL = 0x141 678 | # LP_BEGIN_DROPPOOL = 0x142 679 | LP_DropEnterField = 0x142 680 | LP_DropReleaseAllFreeze = 0x143 681 | LP_DropLeaveField = 0x144 682 | # LP_END_DROPPOOL = 0x144 683 | # LP_BEGIN_MESSAGEBOXPOOL = 0x145 684 | LP_CreateMessgaeBoxFailed = 0x145 685 | LP_MessageBoxEnterField = 0x146 686 | LP_MessageBoxLeaveField = 0x147 687 | # LP_END_MESSAGEBOXPOOL = 0x147 688 | # LP_BEGIN_AFFECTEDAREAPOOL = 0x148 689 | LP_AffectedAreaCreated = 0x148 690 | LP_AffectedAreaRemoved = 0x149 691 | # LP_END_AFFECTEDAREAPOOL = 0x149 692 | # LP_BEGIN_TOWNPORTALPOOL = 0x14A 693 | LP_TownPortalCreated = 0x14A 694 | LP_TownPortalRemoved = 0x14B 695 | # LP_END_TOWNPORTALPOOL = 0x14B 696 | # LP_BEGIN_OPENGATEPOOL = 0x14C 697 | LP_OpenGateCreated = 0x14C 698 | LP_OpenGateRemoved = 0x14D 699 | # LP_END_OPENGATEPOOL = 0x14D 700 | # LP_BEGIN_REACTORPOOL = 0x14E 701 | LP_ReactorChangeState = 0x14E 702 | LP_ReactorMove = 0x14F 703 | LP_ReactorEnterField = 0x150 704 | LP_ReactorLeaveField = 0x151 705 | # LP_END_REACTORPOOL = 0x151 706 | # LP_BEGIN_ETCFIELDOBJ = 0x152 707 | LP_SnowBallState = 0x152 708 | LP_SnowBallHit = 0x153 709 | LP_SnowBallMsg = 0x154 710 | LP_SnowBallTouch = 0x155 711 | LP_CoconutHit = 0x156 712 | LP_CoconutScore = 0x157 713 | LP_HealerMove = 0x158 714 | LP_PulleyStateChange = 0x159 715 | LP_MCarnivalEnter = 0x15A 716 | LP_MCarnivalPersonalCP = 0x15B 717 | LP_MCarnivalTeamCP = 0x15C 718 | LP_MCarnivalResultSuccess = 0x15D 719 | LP_MCarnivalResultFail = 0x15E 720 | LP_MCarnivalDeath = 0x15F 721 | LP_MCarnivalMemberOut = 0x160 722 | LP_MCarnivalGameResult = 0x161 723 | LP_ArenaScore = 0x162 724 | LP_BattlefieldEnter = 0x163 725 | LP_BattlefieldScore = 0x164 726 | LP_BattlefieldTeamChanged = 0x165 727 | LP_WitchtowerScore = 0x166 728 | LP_HontaleTimer = 0x167 729 | LP_ChaosZakumTimer = 0x168 730 | LP_HontailTimer = 0x169 731 | LP_ZakumTimer = 0x16A 732 | # LP_END_ETCFIELDOBJ = 0x16A 733 | # LP_BEGIN_SCRIPT = 0x16B 734 | LP_ScriptMessage = 0x16B 735 | # LP_END_SCRIPT = 0x16B 736 | # LP_BEGIN_SHOP = 0x16C 737 | LP_OpenShopDlg = 0x16C 738 | LP_ShopResult = 0x16D 739 | # LP_END_SHOP = 0x16D 740 | # LP_BEGIN_ADMINSHOP = 0x16E 741 | LP_AdminShopResult = 0x16E 742 | LP_AdminShopCommodity = 0x16F 743 | # LP_END_ADMINSHOP = 0x16F 744 | LP_TrunkResult = 0x170 745 | # LP_BEGIN_STOREBANK = 0x171 746 | LP_StoreBankGetAllResult = 0x171 747 | LP_StoreBankResult = 0x172 748 | # LP_END_STOREBANK = 0x172 749 | LP_RPSGame = 0x173 750 | LP_Messenger = 0x174 751 | LP_MiniRoom = 0x175 752 | # LP_BEGIN_TOURNAMENT = 0x176 753 | LP_Tournament = 0x176 754 | LP_TournamentMatchTable = 0x177 755 | LP_TournamentSetPrize = 0x178 756 | LP_TournamentNoticeUEW = 0x179 757 | LP_TournamentAvatarInfo = 0x17A 758 | # LP_END_TOURNAMENT = 0x17A 759 | # LP_BEGIN_WEDDING = 0x17B 760 | LP_WeddingProgress = 0x17B 761 | LP_WeddingCremonyEnd = 0x17C 762 | LP_END_WEDDING = 0x17C 763 | LP_Parcel = 0x17D 764 | # LP_END_FIELD = 0x17D 765 | # LP_BEGIN_CASHSHOP = 0x17E 766 | LP_CashShopChargeParamResult = 0x17E 767 | LP_CashShopQueryCashResult = 0x17F 768 | LP_CashShopCashItemResult = 0x180 769 | LP_CashShopPurchaseExpChanged = 0x181 770 | LP_CashShopGiftMateInfoResult = 0x182 771 | LP_CashShopCheckDuplicatedIDResult = 0x183 772 | LP_CashShopCheckNameChangePossibleResult = 0x184 773 | LP_CashShopRegisterNewCharacterResult = 0x185 774 | LP_CashShopCheckTransferWorldPossibleResult = 0x186 775 | LP_CashShopGachaponStampItemResult = 0x187 776 | LP_CashShopCashItemGachaponResult = 0x188 777 | LP_CashShopCashGachaponOpenResult = 0x189 778 | LP_ChangeMaplePointResult = 0x18A 779 | LP_CashShopOneADay = 0x18B 780 | LP_CashShopNoticeFreeCashItem = 0x18C 781 | LP_CashShopMemberShopResult = 0x18D 782 | # LP_END_CASHSHOP = 0x18D 783 | # LP_BEGIN_FUNCKEYMAPPED = 0x18E 784 | LP_FuncKeyMappedInit = 0x18E 785 | LP_PetConsumeItemInit = 0x18F 786 | LP_PetConsumeMPItemInit = 0x190 787 | # LP_END_FUNCKEYMAPPED = 0x190 788 | LP_CheckSSN2OnCreateNewCharacterResult = 0x191 789 | LP_CheckSPWOnCreateNewCharacterResult = 0x192 790 | LP_FirstSSNOnCreateNewCharacterResult = 0x193 791 | LP_BEGIN_MAPLETV = 0x194 792 | LP_MapleTVUpdateMessage = 0x195 793 | LP_MapleTVClearMessage = 0x196 794 | LP_MapleTVSendMessageResult = 0x197 795 | LP_BroadSetFlashChangeEvent = 0x198 796 | LP_END_MAPLETV = 0x199 797 | # LP_BEGIN_ITC = 0x19A 798 | LP_ITCChargeParamResult = 0x19A 799 | LP_ITCQueryCashResult = 0x19B 800 | LP_ITCNormalItemResult = 0x19C 801 | # LP_END_ITC = 0x19C 802 | # LP_BEGIN_CHARACTERSALE = 0x19D 803 | LP_CheckDuplicatedIDResultInCS = 0x19D 804 | LP_CreateNewCharacterResultInCS = 0x19E 805 | LP_CreateNewCharacterFailInCS = 0x19F 806 | LP_CharacterSale = 0x1A0 807 | # LP_END_CHARACTERSALE = 0x1A0 808 | # LP_BEGIN_GOLDHAMMER = 0x1A1 809 | LP_GoldHammere_s = 0x1A1 810 | LP_GoldHammerResult = 0x1A2 811 | LP_GoldHammere_e = 0x1A3 812 | # LP_END_GOLDHAMMER = 0x1A3 813 | # LP_BEGIN_BATTLERECORD = 0x1A4 814 | LP_BattleRecord_s = 0x1A4 815 | LP_BattleRecordDotDamageInfo = 0x1A5 816 | LP_BattleRecordRequestResult = 0x1A6 817 | LP_BattleRecord_e = 0x1A7 818 | # LP_END_BATTLERECORD = 0x1A7 819 | # LP_BEGIN_ITEMUPGRADE = 0x1A8 820 | LP_ItemUpgrade_s = 0x1A8 821 | LP_ItemUpgradeResult = 0x1A9 822 | LP_ItemUpgradeFail = 0x1AA 823 | LP_ItemUpgrade_e = 0x1AB 824 | # LP_END_ITEMUPGRADE = 0x1AB 825 | # LP_BEGIN_VEGA = 0x1AC 826 | LP_Vega_s = 0x1AC 827 | LP_VegaResult = 0x1AD 828 | LP_VegaFail = 0x1AE 829 | LP_Vega_e = 0x1AF 830 | # LP_END_VEGA = 0x1AF 831 | LP_LogoutGift = 0x1B0 832 | LP_NO = 0x1B1 833 | -------------------------------------------------------------------------------- /mapy/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import signal 3 | from abc import abstractmethod 4 | from asyncio import ( 5 | Event, 6 | InvalidStateError, 7 | Lock, 8 | get_running_loop, 9 | new_event_loop, 10 | sleep, 11 | ) 12 | from collections import Counter 13 | from datetime import datetime 14 | from inspect import getmembers 15 | from io import BytesIO 16 | from ipaddress import IPv4Address 17 | from pathlib import Path 18 | from socket import AF_INET, IPPROTO_TCP, SOCK_STREAM, TCP_NODELAY, socket 19 | from time import time 20 | from types import new_class 21 | from typing import Any, Coroutine, Literal, TypeAlias 22 | 23 | from yaml import Dumper, Loader, YAMLObject, dump, load 24 | 25 | from .client import WvsGameClient, WvsLoginClient 26 | from .constants import ( 27 | ANTIREPEAT_BUFFS, 28 | Config, 29 | Network, 30 | WorldFlag, 31 | Worlds, 32 | get_job_from_creation, 33 | is_event_vehicle_skill, 34 | ) 35 | from .cpacket import CPacket 36 | from .crypto.maple_iv import MapleIV 37 | from .database.db_client import DatabaseClient 38 | from .game.character import MapleCharacter 39 | from .http_api import http_api 40 | from .logger import Logger 41 | from .opcodes import CRecvOps 42 | from .packet import Packet, PacketHandler, packet_handler 43 | from .scripting import NpcScript 44 | 45 | _RetAddress: TypeAlias = tuple[str, int] 46 | 47 | global _WvsCenter 48 | 49 | 50 | class ClientBase: 51 | def __init__(self, *args, **kwargs) -> None: 52 | self._socket: socket 53 | self._port: int 54 | self._overflow_buff: memoryview | bytearray 55 | self._recv_buff: BytesIO 56 | self._logged_in: bool 57 | self._world_id: int 58 | self._channel_id: int 59 | self._lock: Lock 60 | self._m_riv: MapleIV 61 | self._m_siv: MapleIV 62 | self._parent: "WvsCenter" 63 | self._logger: Logger 64 | self._center: "WvsCenter" 65 | 66 | @property 67 | def connected_channel(self): 68 | ... 69 | 70 | @property 71 | def ip(self) -> IPv4Address: 72 | ... 73 | 74 | @property 75 | def data(self) -> DatabaseClient: 76 | ... 77 | 78 | @property 79 | def identifier(self) -> _RetAddress: 80 | ... 81 | 82 | def dispatch(self, packet) -> None: 83 | ... 84 | 85 | def close(self) -> None: 86 | ... 87 | 88 | async def initialize(self) -> None: 89 | ... 90 | 91 | async def send_packet(self, out_packet) -> None: 92 | ... 93 | 94 | async def send_packet_raw(self, packet) -> None: 95 | ... 96 | 97 | def manipulate_buffer(self, buffer) -> None: 98 | ... 99 | 100 | 101 | class World: 102 | def __init__(self, id): 103 | self._world = Worlds(id) 104 | self._channels = [] 105 | self._flag = WorldFlag.New 106 | self._allow_multi_leveling = Config.ALLOW_MULTI_LEVELING 107 | self._default_creation_slots = Config.DEFAULT_CREATION_SLOTS 108 | self._disable_character_creation = False 109 | self.event_message = Config.DEFAULT_EVENT_MESSAGE 110 | self.ticker_message = Config.DEFAULT_TICKER 111 | self.exp_rate = Config.EXP_RATE 112 | self.quest_exp_rate = Config.QUEST_EXP 113 | self.party_quest_exp_rate = Config.PARTY_QUEST_EXP 114 | self.meso_rate = Config.MESO_RATE 115 | self.drop_rate = Config.DROP_RATE 116 | 117 | @property 118 | def id(self): 119 | return self._world.value 120 | 121 | @property 122 | def name(self): 123 | return self._world.name 124 | 125 | @property 126 | def port(self): 127 | return 8584 + (20 * self._world.value) 128 | 129 | @property 130 | def population(self): 131 | return sum([channel.population for channel in self._channels]) 132 | 133 | @property 134 | def channels(self) -> list["WvsGame"]: 135 | return self._channels 136 | 137 | def add_channel(self, item): 138 | self._channels.append(item) 139 | 140 | def __getitem__(self, key): 141 | for channel in self._channels: 142 | if channel.channel_id == key: 143 | return channel 144 | 145 | return None 146 | 147 | 148 | class ChannelConfig(YAMLObject): 149 | yaml_tag = "!Channel" 150 | channels = Counter() 151 | 152 | __defaults__ = { 153 | "rates": { 154 | "exp": 2.0, 155 | "meso": 1.0, 156 | "drop": 1.5, 157 | "afk_exp": 1.5, 158 | "active_party_exp": 2.0, 159 | "quest_exp": 2.0, 160 | "pq_exp": 1.5, 161 | "mob_respawn_delay": 1.5, 162 | "mob_spawn_count": 0.85, 163 | }, 164 | "combat": { 165 | "boss_damage": 1.5, 166 | "boss_defense": 0.8, 167 | "boss_hp": 2.0, 168 | "mob_hp": 2.0, 169 | "mob_damage": 2, 170 | "boss_exp": 0.7, 171 | "boss_drop": 1.5, 172 | "death_exp_loss": 5.0, 173 | }, 174 | "extra": { 175 | "ticker_message": "Welcome!", 176 | "global_npc": False, 177 | "map_teles": False, 178 | "hyper_rock_limitations": [18000000000], 179 | "enabled_events": ["ZakumPQ", "HorntailPQ", "PinkBean"], 180 | }, 181 | "id": 0, 182 | "port": 8585, 183 | } 184 | 185 | def __setstate__(self, state): 186 | data = ChannelConfig.__defaults__ | state 187 | data_c = data.copy() 188 | for k, v in data_c.items(): 189 | self.__setattr__(k, v) 190 | self.__dict__[k] = v 191 | 192 | def __setitem__(self, key, value): 193 | self.__setattr__(key, value) 194 | self.__dict__[key] = value 195 | 196 | def __getitem__(self, key): 197 | return super().__getattribute__(key) 198 | 199 | def keys(self): 200 | return self.__dict__.keys() 201 | 202 | def __init__(self, **data): 203 | for k, v in (self.__defaults__ | data).items(): 204 | self.__dict__[k] = v 205 | self.__setattr__(k, v) 206 | 207 | def __new__(cls, **data): 208 | dats = cls.__defaults__ | data 209 | dats_c = dats.copy() 210 | for k, v in dats_c.items(): 211 | if isinstance(v, dict): 212 | dc_cls = new_class(k, (dict, object)) 213 | dc_inst = dc_cls() 214 | for dict_key_val, dict_val in v.items(): 215 | setattr(dc_inst, dict_key_val, dict_val) 216 | dc_inst[dict_key_val] = dict_val 217 | dats[k] = dc_inst 218 | 219 | self = super().__new__(cls) 220 | self.__init__(**dats) 221 | return self 222 | 223 | @classmethod 224 | def to_yaml(cls, dumper: Dumper, data): 225 | mappings = {} 226 | for k, v in cls.__defaults__.items(): 227 | if isinstance(v, (int, str)): 228 | mappings[k] = data[k] if k in data else v 229 | if k in cls.__defaults__ and isinstance(v, dict): 230 | mappings[k] = {} 231 | for kk, vv in v.items(): 232 | mappings[k][kk] = data[k][kk] if kk in data[k] else vv 233 | else: 234 | continue 235 | mappings["port"] = data["port"] 236 | mappings["id"] = data["id"] 237 | n = new_class("ChannelConfig", (dict, ChannelConfig, YAMLObject)) 238 | nn = n() 239 | nn.__dict__ = mappings 240 | return dumper.represent_yaml_object("!Channel", nn, cls) 241 | 242 | def __getattribute__(self, __name: str) -> Any: 243 | return super(ChannelConfig, self).__getattribute__(__name) 244 | 245 | def __iter__(self): 246 | return { 247 | k: v for k, v in self.__dict__.items() if not k.startswith("__") 248 | }.__iter__() 249 | 250 | def __get__(self, key): 251 | return getattr(self, key, None) 252 | 253 | @property 254 | def world(self) -> Worlds: 255 | return self.__getattribute__("__world") 256 | 257 | @property 258 | def world_id(self): 259 | return self.world.value 260 | 261 | @world.setter 262 | def world(self, value): 263 | setattr(self, "__world", value) 264 | self.__dict__["__world"] = value 265 | 266 | @property 267 | def world_name(self): 268 | return self.world.name 269 | 270 | @property 271 | def drop(self): 272 | return self.rates.drop # type: ignore 273 | 274 | @property 275 | def exp(self): 276 | return self.rates.exp # type: ignore 277 | 278 | @property 279 | def meso(self): 280 | return self.rates.meso # type: ignore 281 | 282 | @property 283 | def death_penalty(self): 284 | return self.combat.death_exp_loss # type: ignore 285 | 286 | @property 287 | def ticker_message(self): 288 | return self.extra.ticker_message # type: ignore 289 | 290 | @property 291 | def mob_damage(self): 292 | return self.combat.mob_damage # type: ignore 293 | 294 | @property 295 | def mob_health(self): 296 | return self.combat.mob_hp # type: ignore 297 | 298 | @property 299 | def mob_respawn(self): 300 | return self.combat.mob_respawn_delay # type: ignore 301 | 302 | 303 | class WvsCenter: 304 | """Server central coordinator for game / login servers 305 | 306 | Attributes 307 | ---------- 308 | name : str 309 | Server specific name 310 | login : LoginServer 311 | Login server listener instance 312 | _worlds : dict[int, World] 313 | :obj:`dict` of :obj:`int` channel_id -> :obj:`World` server instances 314 | shop: :class:`ShopServer` 315 | Connected `ShopServer` 316 | 317 | """ 318 | 319 | RUNNING = Event() 320 | 321 | def __init__(self): 322 | self._name = "Server Core" 323 | self._loop = new_event_loop() 324 | self._clients = set() 325 | self._pending_logins = [] 326 | self._login = None 327 | self._config = {} 328 | self._start_time = int(time()) 329 | self._shop = None 330 | self._worlds = {} 331 | self._logger = Logger(self) 332 | self._load_config() 333 | 334 | @property 335 | def pending_logins(self): 336 | return self._pending_logins 337 | 338 | @property 339 | def data(self) -> "DatabaseClient": 340 | return NotImplemented 341 | 342 | @property 343 | def login(self) -> "WvsLogin | None": 344 | return self._login 345 | 346 | @property 347 | def logger(self) -> Logger: 348 | return self._logger 349 | 350 | @property 351 | def shop(self) -> "WvsShop | None": 352 | return self._shop 353 | 354 | @shop.setter 355 | def shop(self, shop): 356 | self._shop = shop 357 | 358 | @property 359 | def worlds(self) -> dict[int, World]: 360 | return self._worlds 361 | 362 | def log(self, message) -> None: 363 | self.logger.log_basic(message) 364 | 365 | def _load_config(self): 366 | if not Path("config_test.yaml").exists(): 367 | self._make_config() 368 | 369 | else: 370 | doc = open("config_test.yaml", "r") 371 | self._config = load(doc, Loader) 372 | doc.close() 373 | 374 | def _make_config(self): 375 | self._config = { 376 | "database": { 377 | "host": "127.0.0.1", 378 | "port": 5432, 379 | "user": "postgres", 380 | "password": "smoqueed420", 381 | "database": "mapy", 382 | }, 383 | "game": { 384 | "worlds": [ 385 | { 386 | "id": w.value, 387 | "name": w.name.lower(), 388 | "channels": [ 389 | ChannelConfig(id=c, port=9494 + (c + (w.value * 20))) 390 | for c in range(Network.CHANNEL_COUNT) 391 | ], 392 | } 393 | for w in Network.ACTIVE_WORLDS 394 | ] 395 | }, 396 | } 397 | self.save_config() 398 | 399 | def save_config(self): 400 | with open("config_test.yaml", "w") as f: 401 | dump(self._config, f, default_flow_style=False, width=88) 402 | 403 | def _run(self): 404 | if self.RUNNING.is_set(): 405 | return 406 | 407 | loop = self._loop 408 | try: 409 | loop.add_signal_handler(signal.SIGINT, self._loop.stop) 410 | loop.add_signal_handler(signal.SIGTERM, self._loop.stop) 411 | except NotImplementedError: 412 | pass 413 | 414 | def stop_loop_on_completion(f): 415 | loop.stop() 416 | 417 | future = loop.create_task(self._start()) 418 | future.add_done_callback(stop_loop_on_completion) 419 | 420 | try: 421 | loop.run_forever() 422 | 423 | except KeyboardInterrupt: 424 | self.log("Received signal to terminate event loop") 425 | # loop.run_until_complete(self.data.stop()) 426 | 427 | finally: 428 | future.remove_done_callback(stop_loop_on_completion) 429 | self.save_config() 430 | loop.run_until_complete(self._loop.shutdown_asyncgens()) 431 | self.log(f"Closed {self._name}") 432 | 433 | async def _start(self): 434 | self.RUNNING.set() 435 | self._start_time = int(time()) 436 | self.log("Initializing Server") 437 | 438 | self._login = WvsLogin() 439 | await self._login.run() 440 | 441 | # world_id = Worlds.SCANIA.value 442 | 443 | for _world in self._config["game"]["worlds"]: 444 | world_enum = Worlds(_world["id"]) 445 | world = World(world_enum.value) 446 | 447 | for channel_config in _world["channels"]: 448 | channel_config.world = world_enum 449 | channel = WvsGame(**channel_config) 450 | await channel.run() 451 | world.add_channel(channel) 452 | 453 | self.worlds[world_enum.value] = world 454 | 455 | async def startup(): 456 | http_api.ctx.center = _WvsCenter 457 | http = await http_api.create_server( 458 | host="127.0.0.1", 459 | port=12345, 460 | noisy_exceptions=True, 461 | return_asyncio_server=True, 462 | debug=True, 463 | ) 464 | if http: 465 | await http.startup() 466 | 467 | self._loop.create_task(startup()) 468 | 469 | while get_running_loop().is_running(): 470 | await sleep(600.0) 471 | 472 | @property 473 | def uptime(self): 474 | return int(time()) - self._start_time 475 | 476 | @property 477 | def population(self): 478 | login_population = 0 479 | if self._login: 480 | login_population += self._login.population 481 | 482 | return sum(w.population for _, w in self.worlds.items()) + login_population 483 | 484 | 485 | class ServerBase: 486 | """Server base for center, channel, and login servers""" 487 | 488 | __slots__ = ( 489 | "_name", 490 | "_clients", 491 | "_port", 492 | "_packet_handlers", 493 | "_ready", 494 | "_serv_sock", 495 | "_acceptor", 496 | "_logger", 497 | "_loop", 498 | "_alive", 499 | "_center", 500 | ) 501 | 502 | def __init__(self): 503 | self._name = "" 504 | self._loop = get_running_loop() 505 | self._logger: Logger = Logger(self) 506 | self._clients = [] 507 | self._packet_handlers = [] 508 | self._ready = Event() 509 | if not self._port: 510 | self._port = 9494 511 | 512 | self._alive = Event() 513 | self._acceptor = None 514 | self._serv_sock = socket(AF_INET, SOCK_STREAM) 515 | self._serv_sock.setblocking(False) 516 | self._serv_sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) 517 | self._serv_sock.bind(("127.0.0.1", self._port)) 518 | self._serv_sock.listen(0) 519 | self.add_packet_handlers() 520 | 521 | @property 522 | def center(self) -> WvsCenter | None: 523 | if not _WvsCenter: 524 | return None 525 | return _WvsCenter 526 | 527 | @property 528 | def port(self): 529 | return self._port 530 | 531 | @port.setter 532 | def port(self, value): 533 | self._port = value 534 | 535 | def log(self, message, level=None): 536 | if not self._logger: 537 | return 538 | 539 | if not level: 540 | return self._logger.log_basic(message) 541 | else: 542 | return self._logger.log(str(level).upper(), message) 543 | 544 | @abstractmethod 545 | async def client_connect(self, client_sock) -> ClientBase: 546 | return NotImplemented 547 | 548 | @property 549 | def alive(self) -> bool: 550 | return self._alive.is_set() 551 | 552 | async def run(self): 553 | self._alive.set() 554 | self._ready.set() 555 | self._acceptor = self._loop.create_task(self.listen()) 556 | 557 | def close(self) -> None: 558 | if self._acceptor: 559 | self._acceptor.cancel() 560 | else: 561 | raise InvalidStateError( 562 | "The acceptor is not currently running or accepting connections." 563 | ) 564 | 565 | async def on_client_accepted(self, socket: socket): 566 | if not _WvsCenter: 567 | return 568 | 569 | client = await self.client_connect(socket) 570 | 571 | if not client: 572 | self.log(f"socket ({socket.getpeername}) connecting failed.") 573 | return 574 | 575 | self.log(f"{self.name} Accepted {client.ip}") 576 | 577 | _WvsCenter._clients.add(client) 578 | self._clients.append(client) 579 | 580 | # Dispatch accept packet to client and begin client socket loop 581 | await client.initialize() 582 | 583 | async def on_client_disconnect(self, client): 584 | self._clients.remove(client) 585 | 586 | self.log(f"Client Disconnected {client.ip}") 587 | 588 | def add_packet_handlers(self): 589 | members = getmembers(self) 590 | for _, member in members: 591 | # Register all packet handlers for inheriting server 592 | if ( 593 | isinstance(member, PacketHandler) 594 | and member not in self._packet_handlers 595 | ): 596 | self._packet_handlers.append(member) 597 | 598 | def wait_until_ready(self) -> Coroutine[Any, Any, Literal[True]]: 599 | """Block event loop until the GameServer has started listening for clients.""" 600 | return self._ready.wait() 601 | 602 | async def listen(self): 603 | self.log(f"Listening on port {self._port}", "info") 604 | 605 | while self._alive.is_set(): 606 | client_sock, _ = await self._loop.sock_accept(self._serv_sock) 607 | client_sock.setblocking(False) 608 | client_sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) 609 | self._loop.create_task(self.on_client_accepted(client_sock)) 610 | 611 | @property 612 | def data(self): 613 | if _WvsCenter: 614 | return _WvsCenter.data 615 | 616 | @property 617 | def name(self) -> str: 618 | return self._name 619 | 620 | @property 621 | def population(self) -> int: 622 | return len(self._clients) 623 | 624 | def push(self, client: ClientBase, packet: Packet): 625 | self._logger.log_ipkt( 626 | f"{self.name} {packet.name} {client.ip} {packet.to_string()}" 627 | ) 628 | 629 | for __packet_handler in self._packet_handlers: 630 | if __packet_handler.op_code == packet.op_code: 631 | get_running_loop().create_task( 632 | packet_handler.callback(self, client, packet) 633 | ) 634 | break 635 | else: 636 | self.log( 637 | f"{self.name} Unhandled event in : {packet.name}", "WARNING" 638 | ) 639 | 640 | 641 | class WvsGame(ServerBase, ChannelConfig): 642 | def __new__(cls, **data): 643 | self = super().__new__(cls, **data) 644 | return self 645 | 646 | def __init__(self, **data): 647 | for k, v in data.items(): 648 | setattr(self, k, v) 649 | 650 | self.channels.update([self.world.value]) 651 | self.channel_id = self.id 652 | self._field_manager = {} 653 | super().__init__() 654 | 655 | async def client_connect(self, client): 656 | if not _WvsCenter: 657 | return 658 | 659 | game_client = WvsGameClient(self, client) 660 | _WvsCenter._clients.append(game_client) # type: ignore 661 | return game_client 662 | 663 | async def on_client_disconnect(self, client): 664 | if not _WvsCenter: 665 | return 666 | 667 | if client.logged_in: 668 | field = await client.get_field() 669 | field.clients.remove(client) 670 | # field.sockets.pop(client.character) 671 | await field.broadcast(CPacket.user_leave_field(client.character)) 672 | 673 | _WvsCenter._clients.remove(client) 674 | await super().on_client_disconnect(client) 675 | 676 | async def get_field(self, field_id): 677 | if not _WvsCenter or not self.data: 678 | return 679 | 680 | field = self._field_manager.get(field_id, None) 681 | if field: 682 | return field 683 | 684 | field = await self.data.field.get(field_id) 685 | self._field_manager[field_id] = field 686 | 687 | return field 688 | 689 | @packet_handler(CRecvOps.CP_MigrateIn) 690 | async def handle_migrate_in(self, client, packet): 691 | if not _WvsCenter or not self.data: 692 | return 693 | 694 | uid = packet.decode_int() 695 | packet.decode_buffer(16) # Machine ID 696 | packet.decode_byte() # is gm 697 | packet.decode_byte() 698 | packet.decode_long() # Session ID 699 | 700 | login_req: PendingLogin | None 701 | 702 | for x in _WvsCenter._pending_logins: 703 | if x.character.id == uid: 704 | login_req = x 705 | break 706 | 707 | else: 708 | login_req = None 709 | 710 | if not login_req: 711 | return await client.disconnect() 712 | 713 | login_req.migrated = True 714 | login_req.character._client = client 715 | 716 | client.character = await self.data.characters.load(uid, client) 717 | field = await self.get_field(client.character.field_id) 718 | if not field: 719 | return 720 | 721 | await field.add(client) 722 | 723 | await client.send_packet(CPacket.claim_svr_changed(True)) 724 | await client.send_packet(CPacket.set_gender(client.character.stats.gender)) 725 | await client.send_packet(CPacket.func_keys_init(client.character.func_keys)) 726 | await client.send_packet(CPacket.broadcast_server_msg(self.ticker_message)) 727 | 728 | @packet_handler(CRecvOps.CP_UserMove) 729 | async def handle_user_move(self, client, packet): 730 | packet.decode_long() # v1 731 | packet.decode_byte() # portal count 732 | packet.decode_long() # v2 733 | packet.decode_int() # map crc 734 | packet.decode_int() # key 735 | packet.decode_int() # key crc 736 | 737 | move_path = packet.decode_buffer(-1) 738 | client.character.position.decode_move_path(move_path) 739 | await client.broadcast(CPacket.user_movement(client.character.id, move_path)) 740 | 741 | @packet_handler(CRecvOps.CP_UserSkillUseRequest) 742 | async def handle_skill_use_request(self, client, packet): 743 | packet.decode_int() # tick count 744 | skill_id = packet.decode_int() 745 | _ = packet.decode_byte() 746 | 747 | if skill_id in ANTIREPEAT_BUFFS: 748 | packet.decode_short() # x 749 | packet.decode_short() # y 750 | 751 | if skill_id == 4131006: 752 | packet.decode_int() 753 | 754 | if is_event_vehicle_skill(skill_id): 755 | packet.skip(1) 756 | if skill_id == 2311001: 757 | packet.skip(2) 758 | 759 | packet.decode_short() 760 | 761 | casted = False 762 | if skill_id: 763 | casted = await client.character.skills.cast(skill_id) 764 | 765 | await client.send_packet(CPacket.enable_actions()) 766 | 767 | if casted: 768 | client.broadcast( 769 | CPacket.effect_remote( 770 | client.character.obj_id, 771 | 1, 772 | skill_id, 773 | client.character.stats.level, 774 | 1, 775 | ) 776 | ) 777 | 778 | @packet_handler(CRecvOps.CP_UserSelectNpc) 779 | async def handle_user_select_npc(self, client, packet): 780 | obj_id = packet.decode_int() 781 | packet.decode_short() # x 782 | packet.decode_short() # y 783 | 784 | if client.npc_script: 785 | ... # client has npc script already? 786 | 787 | npc = client.character.field.npcs.get(obj_id) 788 | 789 | if npc: 790 | client.npc_script = NpcScript.get_script(npc.id, client) 791 | await client.npc_script.execute() 792 | 793 | @packet_handler(CRecvOps.CP_UserScriptMessageAnswer) 794 | async def handle_user_script_message_answer(self, client, packet): 795 | script = client.npc_script 796 | 797 | type_ = packet.decode_byte() 798 | type_expected = script.last_msg_type 799 | 800 | if type_ != type_expected: 801 | self.log( 802 | f"User answered type: [{type_}], expected [{type_expected}]" "debug" 803 | ) 804 | return 805 | 806 | resp = packet.decode_byte() 807 | self.log(f"Script response: [{resp}]") 808 | 809 | if type_ == 0: 810 | if resp == 255: 811 | script.end_chat() 812 | elif resp == 0: 813 | await script.proceed_back() 814 | elif resp == 1: 815 | await script.proceed_next(resp) 816 | 817 | @packet_handler(CRecvOps.CP_UpdateGMBoard) 818 | async def handle_update_gm_board(self, client, packets): 819 | ... 820 | 821 | @packet_handler(CRecvOps.CP_RequireFieldObstacleStatus) 822 | async def handle_require_field_obstacle(self, client, packets): 823 | ... 824 | 825 | 826 | class PendingLogin: 827 | def __init__(self, character, account, requested): 828 | self.character = character 829 | self.char_id = character.id 830 | self.account = account 831 | self.requested = requested 832 | self.migrated = False 833 | 834 | 835 | class WvsLogin(ServerBase): 836 | def __init__(self): 837 | self.port = Network.LOGIN_PORT 838 | super().__init__() 839 | self._worlds = [] 840 | self._auto_register = Config.AUTO_REGISTER 841 | self._request_pin = Config.REQUEST_PIN 842 | self._request_pic = Config.REQUEST_PIC 843 | self._require_staff_ip = Config.REQUIRE_STAFF_IP 844 | self._max_characters = Config.MAX_CHARACTERS 845 | self._login_pool = [] 846 | 847 | def add_world(self, world): 848 | self._worlds.append(world) 849 | 850 | async def client_connect(self, client): 851 | return WvsLoginClient(self, client) 852 | 853 | @packet_handler(CRecvOps.CP_CreateSecurityHandle) 854 | async def create_secuirty_heandle(self, client, packet): 855 | if Config.AUTO_LOGIN: 856 | packet = Packet(op_code=CRecvOps.CP_CheckPassword) 857 | packet.encode_string("admin") 858 | packet.encode_string("admin") 859 | packet.seek(2) 860 | client.dispatch(packet) 861 | 862 | @packet_handler(CRecvOps.CP_CheckPassword) 863 | async def check_password(self, client, packet): 864 | password = packet.decode_string() 865 | username = packet.decode_string() 866 | response = await client.login(username, password) 867 | 868 | if response == 0: 869 | return await client.send_packet( 870 | CPacket.check_password_result(client, response) 871 | ) 872 | 873 | await client.send_packet(CPacket.check_password_result(response=response)) 874 | 875 | async def send_world_information(self, client) -> None: 876 | for world in _WvsCenter.worlds: 877 | await client.send_packet(CPacket.world_information(world)) 878 | 879 | await client.send_packet(CPacket.end_world_information()) 880 | await client.send_packet(CPacket.last_connected_world(0)) 881 | 882 | await client.send_packet(CPacket.send_recommended_world(self._worlds)) 883 | 884 | @packet_handler(CRecvOps.CP_WorldRequest) 885 | async def world_request(self, client, packet): 886 | await self.send_world_information(client) 887 | 888 | @packet_handler(CRecvOps.CP_WorldInfoRequest) 889 | async def world_info_request(self, client, packet): 890 | await self.send_world_information(client) 891 | 892 | @packet_handler(CRecvOps.CP_CheckUserLimit) 893 | async def check_user_limit(self, client, packet): 894 | await client.send_packet(CPacket.check_user_limit(0)) 895 | 896 | @packet_handler(CRecvOps.CP_SelectWorld) 897 | async def select_world(self, client, packet): 898 | packet.decode_byte() 899 | 900 | client.world_id = world_id = packet.decode_byte() 901 | client.channel_id = packet.decode_byte() 902 | 903 | client.account.last_connected_world = world_id 904 | 905 | # Load avatars for specific world in future 906 | await client.load_avatars(world_id=world_id) 907 | await client.send_packet(CPacket.world_result(client.avatars)) 908 | 909 | @packet_handler(CRecvOps.CP_LogoutWorld) 910 | async def logout_world(self, client, packet): 911 | pass 912 | 913 | @packet_handler(CRecvOps.CP_CheckDuplicatedID) 914 | async def check_duplicated_id(self, client, packet): 915 | name = packet.decode_string() 916 | exists = await _WvsCenter.data.characters.get(name=name) is not None 917 | 918 | await client.send_packet(CPacket.check_duplicated_id_result(name, exists)) 919 | 920 | @packet_handler(CRecvOps.CP_ViewAllChar) 921 | async def view_all_characters(self, client, packet): 922 | await client.load_avatars() 923 | packet.decode_byte() # game_start_mode 924 | 925 | await asyncio.sleep(2) 926 | await client.send_packet(CPacket.start_view_all_characters(client.avatars)) 927 | 928 | for world in _WvsCenter.worlds: 929 | await client.send_packet(CPacket.view_all_characters(world, client.avatars)) 930 | 931 | @packet_handler(CRecvOps.CP_CreateNewCharacter) 932 | async def create_new_character(self, client, packet): 933 | character = MapleCharacter() 934 | character.stats.name = packet.decode_string() 935 | character.stats.job = get_job_from_creation(packet.decode_int()) 936 | character.stats.sub_job = packet.decode_short() 937 | character.stats.face = packet.decode_int() 938 | character.stats.hair = packet.decode_int() + packet.decode_int() 939 | character.stats.skin = packet.decode_int() 940 | 941 | invs = character.inventories 942 | 943 | top = await _WvsCenter.data.items.get(packet.decode_int()) 944 | bottom = await _WvsCenter.data.items.get(packet.decode_int()) 945 | shoes = await _WvsCenter.data.items.get(packet.decode_int()) 946 | weapon = await _WvsCenter.data.items.get(packet.decode_int()) 947 | 948 | invs.add(top, slot=-5) 949 | invs.add(bottom, slot=-6) 950 | invs.add(shoes, slot=-7) 951 | invs.add(weapon, slot=-11) 952 | 953 | character.stats.gender = packet.decode_byte() 954 | 955 | character_id = await _WvsCenter.data.account( 956 | id=client.account.id 957 | ).create_character(character) 958 | 959 | if character_id: 960 | character.stats.id = character_id 961 | client.avatars.append(character) 962 | 963 | return await client.send_packet( 964 | CPacket.create_new_character(character, False) 965 | ) 966 | 967 | return await client.send_packet(CPacket.create_new_character(character, True)) 968 | 969 | @packet_handler(CRecvOps.CP_SelectCharacter) 970 | async def select_character(self, client, packet): 971 | if not _WvsCenter: 972 | return 973 | 974 | uid = packet.decode_int() 975 | character = next((c for c in client.avatars if c.id == uid), None) 976 | channel = _WvsCenter.worlds[client.world_id][client.channel_id] 977 | if not channel: 978 | return 979 | 980 | port = channel.port 981 | 982 | _WvsCenter.pending_logins.append( 983 | PendingLogin(character, client.account, datetime.now()) 984 | ) 985 | 986 | await client.send_packet(CPacket.select_character_result(uid, port)) 987 | 988 | 989 | class WvsShop(ServerBase): 990 | def __init__(self): 991 | pass 992 | 993 | 994 | if ( 995 | not locals().get("_WvsCenter", None) 996 | and not globals().get("_WvsCenter", None) 997 | and not WvsCenter.RUNNING.is_set() 998 | ): 999 | _WvsCenter = WvsCenter() 1000 | _WvsCenter._run() 1001 | --------------------------------------------------------------------------------