start repo

This commit is contained in:
Ra 2022-03-09 13:48:56 -07:00
commit 61b623c5d2
16 changed files with 1404 additions and 0 deletions

5
.gitignore vendored Normal file

@ -0,0 +1,5 @@
**/.git
**/.vscode
**/__pyccache__
**/test*.*

5
__init__.py Normal file

@ -0,0 +1,5 @@
__all__ = "ClientBase", "Client", "Packet", "iPacket", "oPacket"
from .client_base import ClientBase
from .client import Client
from .packet import Packet, iPacket, oPacket

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
client.py Normal file

@ -0,0 +1,16 @@
from .client_base import ClientBase
from .packet import packet_handler, oPacket
from .opcodes import RecvOps, SendOps
class Client(ClientBase):
async def begin(self):
# opkt = oPacket(SendOps.LOGIN_PASSWORD)
...
@packet_handler(RecvOps.PING)
async def pong(self, ipkt):
print("Sending PONG packet")
opkt = oPacket(SendOps.PONG)
await self.send_packet(opkt)

114
client_base.py Normal file

@ -0,0 +1,114 @@
from asyncio import Event, get_event_loop, get_running_loop, run_coroutine_threadsafe, sleep, Lock
from socket import AF_INET, IPPROTO_TCP, SOCK_STREAM, TCP_NODELAY, socket
from .packet import PacketHandler, Packet, iPacket, oPacket
from .crypto import MapleAes, decrypt_transform, encrypt_transform, MapleIV
from rich import print
VERSION = 111
class ClientBase:
def __init__(self, loop=None):
self._loop = loop or get_event_loop()
self._packet_handlers = []
self._ready = Event()
self._lock = Lock()
self._sock = socket(AF_INET, SOCK_STREAM)
self._sock.setblocking(False)
self._sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
self._version = None
self._sub_version = None
self._locale = None
self._recv_iv = None
self._send_iv = None
self._buff = bytearray()
self._recv_task = None
self.add_packet_handlers()
async def start(self):
self._recv_task = self._loop.create_task(self._sock_recv())
self._ready.set()
async def send_packet(self, opacket, raw=False):
opkt = bytearray(opacket.getvalue())
length = len(opkt)
# buf = memoryview(opkt)
header = MapleAes.get_header(self._send_iv, length, self._version)
encrypt_transform(opkt)
opkt = MapleAes.transform(opkt, self._send_iv)
await self._loop.sock_sendall(self._sock, header + opkt)
async def _sock_recv(self):
await self._loop.sock_connect(self._sock, ("51.222.56.169", 8485))
await self._ready.wait()
nbytes = -1
while self._loop.is_running() and self._sock.fileno():
self._buff.extend((await self._loop.sock_recv(self._sock, nbytes)))
if not self._send_iv and self._buff:
begin_packet = iPacket(self._buff)
self._version = begin_packet.decode_short()
self._sub_version = begin_packet.decode_string()
self._send_iv = MapleIV(begin_packet.decode_int())
self._recv_iv = MapleIV(begin_packet.decode_int())
self._locale = begin_packet.decode_byte()
await getattr(self, "begin")()
print(
f"Version: {self._version} | Sub Version: {self._sub_version} | Locale: {self._locale} | Send IV: {self._send_iv} | Recv IV: {self._recv_iv}"
)
self._buff = bytearray()
continue
if self._buff:
length = MapleAes.get_length(self._buff[0:4])
if length != len(self._buff[4:]):
nbytes = length - len(self._buff[4:])
else:
nbytes = -1
buf = bytearray(self._buff)[4:]
if length == len(buf):
buf = MapleAes.transform(buf, self._recv_iv)
buf = decrypt_transform(buf)
packet = iPacket(buf)
self._buff = bytearray()
self.handle_packet(packet)
def handle_packet(self, packet):
coro = None
for packet_handler in self._packet_handlers:
if packet_handler.op_code == packet.op_code:
coro = packet_handler.callback
break
else:
print(f"Unhandled packet: {packet.op_code}")
return
if not coro:
raise AttributeError
print(f"Queueing packet for consumption: {packet.name}")
self._loop.create_task(coro(self, packet))
def add_packet_handlers(self):
import inspect
members = inspect.getmembers(self)
for _, member in members:
if (isinstance(member, PacketHandler)
and member not in self._packet_handlers):
self._packet_handlers.append(member)
async def begin(self):
raise NotImplementedError

238
crypto.py Normal file

@ -0,0 +1,238 @@
from Crypto.Cipher import AES
# from struct import unpack, pack
class MapleAes:
_user_key = bytearray([
0x13, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00,
0xB4, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00,
0x33, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00
])
@classmethod
def transform(cls, buffer, iv):
remaining = len(buffer)
length = 0x5B0
start = 0
real_iv = bytearray(16)
iv_bytes = [
iv.value & 255,
iv.value >> 8 & 255,
iv.value >> 16 & 255,
iv.value >> 24 & 255,
]
while remaining > 0:
for index in range(len(real_iv)):
real_iv[index] = iv_bytes[index % 4] # type: ignore
if remaining < length:
length = remaining
index = start
while index < start + length:
sub = index - start
if (sub % 16) == 0:
real_iv = AES.new(cls._user_key,
AES.MODE_ECB).encrypt(real_iv)
buffer[index] ^= real_iv[sub % 16]
index += 1
start += length
remaining -= length
length = 0x5B4
iv.shuffle()
return buffer
@staticmethod
def get_header(iv, length, major_ver):
first = -(major_ver + 1) ^ iv.hiword
second = (first + 2**16) ^ length
return bytearray([
first & 0xFF, first >> 8 & 0xFF, second & 0xFF, second >> 8 & 0xFF
])
@staticmethod
def get_length(data):
return ((data[1] << 8) + data[0]) ^ ((data[3] << 8) + data[2])
class MapleIV:
_shuffle = bytearray([
0xEC, 0x3F, 0x77, 0xA4, 0x45, 0xD0, 0x71, 0xBF, 0xB7, 0x98, 0x20, 0xFC,
0x4B, 0xE9, 0xB3, 0xE1, 0x5C, 0x22, 0xF7, 0x0C, 0x44, 0x1B, 0x81, 0xBD,
0x63, 0x8D, 0xD4, 0xC3, 0xF2, 0x10, 0x19, 0xE0, 0xFB, 0xA1, 0x6E, 0x66,
0xEA, 0xAE, 0xD6, 0xCE, 0x06, 0x18, 0x4E, 0xEB, 0x78, 0x95, 0xDB, 0xBA,
0xB6, 0x42, 0x7A, 0x2A, 0x83, 0x0B, 0x54, 0x67, 0x6D, 0xE8, 0x65, 0xE7,
0x2F, 0x07, 0xF3, 0xAA, 0x27, 0x7B, 0x85, 0xB0, 0x26, 0xFD, 0x8B, 0xA9,
0xFA, 0xBE, 0xA8, 0xD7, 0xCB, 0xCC, 0x92, 0xDA, 0xF9, 0x93, 0x60, 0x2D,
0xDD, 0xD2, 0xA2, 0x9B, 0x39, 0x5F, 0x82, 0x21, 0x4C, 0x69, 0xF8, 0x31,
0x87, 0xEE, 0x8E, 0xAD, 0x8C, 0x6A, 0xBC, 0xB5, 0x6B, 0x59, 0x13, 0xF1,
0x04, 0x00, 0xF6, 0x5A, 0x35, 0x79, 0x48, 0x8F, 0x15, 0xCD, 0x97, 0x57,
0x12, 0x3E, 0x37, 0xFF, 0x9D, 0x4F, 0x51, 0xF5, 0xA3, 0x70, 0xBB, 0x14,
0x75, 0xC2, 0xB8, 0x72, 0xC0, 0xED, 0x7D, 0x68, 0xC9, 0x2E, 0x0D, 0x62,
0x46, 0x17, 0x11, 0x4D, 0x6C, 0xC4, 0x7E, 0x53, 0xC1, 0x25, 0xC7, 0x9A,
0x1C, 0x88, 0x58, 0x2C, 0x89, 0xDC, 0x02, 0x64, 0x40, 0x01, 0x5D, 0x38,
0xA5, 0xE2, 0xAF, 0x55, 0xD5, 0xEF, 0x1A, 0x7C, 0xA7, 0x5B, 0xA6, 0x6F,
0x86, 0x9F, 0x73, 0xE6, 0x0A, 0xDE, 0x2B, 0x99, 0x4A, 0x47, 0x9C, 0xDF,
0x09, 0x76, 0x9E, 0x30, 0x0E, 0xE4, 0xB2, 0x94, 0xA0, 0x3B, 0x34, 0x1D,
0x28, 0x0F, 0x36, 0xE3, 0x23, 0xB4, 0x03, 0xD8, 0x90, 0xC8, 0x3C, 0xFE,
0x5E, 0x32, 0x24, 0x50, 0x1F, 0x3A, 0x43, 0x8A, 0x96, 0x41, 0x74, 0xAC,
0x52, 0x33, 0xF0, 0xD9, 0x29, 0x80, 0xB1, 0x16, 0xD3, 0xAB, 0x91, 0xB9,
0x84, 0x7F, 0x61, 0x1E, 0xCF, 0xC5, 0xD1, 0x56, 0x3D, 0xCA, 0xF4, 0x05,
0xC6, 0xE5, 0x08, 0x49
])
def __init__(self, vector):
self.value = vector
def __int__(self):
return self.value
def __str__(self):
return self.value
@property
def hiword(self):
return self.value >> 16
@property
def loword(self):
return self.value
def shuffle(self):
seed = [0xF2, 0x53, 0x50, 0xC6]
p_iv = self.value
for i in range(4):
temp_p_iv = (p_iv >> (8 * i)) & 0xFF
a = seed[1]
b = a
b = self._shuffle[b & 0xFF]
b -= temp_p_iv
seed[0] += b
b = seed[2]
b ^= self._shuffle[int(temp_p_iv) & 0xFF]
a -= int(b) & 0xFF
seed[1] = a
a = seed[3]
b = a
a -= seed[0] & 0xFF
b = self._shuffle[b & 0xFF]
b += temp_p_iv
b ^= seed[2]
seed[2] = b & 0xFF
a += self._shuffle[temp_p_iv & 0xFF] & 0xFF
seed[3] = a
c = seed[0] & 0xFF
c |= (seed[1] << 8) & 0xFFFF
c |= (seed[2] << 16) & 0xFFFFFF
c |= (seed[3] << 24) & 0xFFFFFFFF
c = (c << 0x03) | (c >> 0x1D)
seed[0] = c & 0xFF
seed[1] = (c >> 8) & 0xFFFF
seed[2] = (c >> 16) & 0xFFFFFF
seed[3] = (c >> 24) & 0xFFFFFFFF
c = seed[0] & 0xFF
c |= (seed[1] << 8) & 0xFFFF
c |= (seed[2] << 16) & 0xFFFFFF
c |= (seed[3] << 24) & 0xFFFFFFFF
self.value = c
def decrypt_transform(data):
for j in range(1, 7):
remember = 0
data_length = len(data) & 0xFF
next_remember = 0
if j % 2 == 0:
for i in range(len(data)):
cur = data[i]
cur = (cur - 0x48) & 0xFF
cur = ~cur & 0xFF
cur = roll_left(cur, data_length & 0xFF)
next_remember = cur
cur ^= remember
remember = next_remember
cur = (cur - data_length) & 0xFF
cur = roll_right(cur, 3)
data[i] = cur
data_length -= 1
else:
for i in reversed(range(len(data))):
cur = data[i]
cur = roll_left(cur, 3)
cur ^= 0x13
next_remember = cur
cur ^= remember
remember = next_remember
cur = (cur - data_length) & 0xFF
cur = roll_right(cur, 4) & 0xFF
data[i] = cur
data_length -= 1
return data
def encrypt_transform(data):
b = {str(i): 0 for i in range(len(data))}
cur = 0
for _ in range(3):
length = len(data) & 0xFF
xor_key = 0
i = 0
while i < len(data):
cur = roll_left(data[i], 3)
cur = cur + length
cur = (cur ^ xor_key) & 0xFF
xor_key = cur
cur = ~roll_right(cur, length & 0xFF) & 0xFF
cur = (cur + 0x48) & 0xFF
data[i] = cur
b[str(i)] = cur
length -= 1
i += 1
xor_key = 0
length = len(data) & 0xFF
i = len(data) - 1
while i >= 0:
cur = roll_left(data[i], 4)
cur += length
cur = (cur ^ xor_key) & 0xFF
xor_key = cur
cur ^= 0x13
cur = roll_right(cur, 3)
data[i] = cur
b[str(i)] = cur
length -= 1
i -= 1
return bytearray([b[b_] for b_ in b])
def roll_left(value, shift):
num = value << (shift % 8)
return (num & 0xFF) | (num >> 8)
def roll_right(value, shift):
num = (value << 8) >> (shift % 8)
return (num & 0xFF) | (num >> 8)

725
opcodes.py Normal file

@ -0,0 +1,725 @@
from enum import IntEnum
class RecvOps(IntEnum):
LOGIN_STATUS = 0x00
SEND_LINK = 0x01
LOGIN_SECOND = 0x02
SERVERSTATUS = 0x03
GENDER_SET = 0x04
PIN_OPERATION = 0x06
PIN_ASSIGNED = 0x07
ALL_CHARLIST = 0x08
SERVERLIST = 0x0A
CHARLIST = 0x0B
SERVER_IP = 0x0C
CHAR_NAME_RESPONSE = 0x0D
ADD_NEW_CHAR_ENTRY = 0x0E
DELETE_CHAR_RESPONSE = 0x0F
CHANGE_CHANNEL = 0x10
PING = 0x11
CS_USE = 0x12
CHANNEL_SELECTED = 0x14
RELOG_RESPONSE = 0x16
ENABLE_RECOMMENDED = 0x1B
SEND_RECOMMENDED = 0x1C
SPECIAL_CREATION = 0x1E
SECONDPW_ERROR = 0x1F
INVENTORY_OPERATION = 0x20
INVENTORY_GROW = 0x21
UPDATE_STATS = 0x22
GIVE_BUFF = 0x23
CANCEL_BUFF = 0x24
TEMP_STATS = 0x25
TEMP_STATS_RESET = 0x26
UPDATE_SKILLS = 0x27
SKILL_MEMORY = 0x28
UPDATE_SKILL_TICK = 0x29
FAME_RESPONSE = 0x2B
SHOW_STATUS_INFO = 0x2C
GAME_PATCHES = 0x2D
SHOW_NOTES = 0x2E
TROCK_LOCATIONS = 0x2F
LIE_DETECTOR = 0x30
BOMB_LIE_DETECTOR = 0x31
REPORT_RESPONSE = 0x33
REPORT_TIME = 0x34
REPORT_STATUS = 0x35
UPDATE_MOUNT = 0x36
SHOW_QUEST_COMPLETION = 0x37
SEND_TITLE_BOX = 0x38
USE_SKILL_BOOK = 0x39
SP_RESET = 0x3A
AP_RESET = 0x3B
DISTRIBUTE_ITEM = 0x3D
EXPAND_CHARACTER_SLOTS = 0x3E
FINISH_SORT = 0x3F
FINISH_GATHER = 0x40
REPORT_RESULT = 0x42
TRADE_LIMIT = 0x44
UPDATE_GENDER = 0x45
BBS_OPERATION = 0x46
CHAR_INFO = 0x49
PARTY_OPERATION = 0x4A
MEMBER_SEARCH = 0x4B
PARTY_SEARCH = 0x4C
BOOK_INFO = 0x4D
EXPEDITION_OPERATION = 0x4F
BUDDYLIST = 0x50
GUILD_OPERATION = 0x52
ALLIANCE_OPERATION = 0x53
SPAWN_PORTAL = 0x54
MECH_PORTAL = 0x55
ECHO_MESSAGE = 0x56
SERVERMESSAGE = 0x58
PIGMI_REWARD = 0x59
OWL_OF_MINERVA = 0x5A
OWL_RESULT = 0x5B
ENGAGE_REQUEST = 0x5C
ENGAGE_RESULT = 0x5D
WEDDING_GIFT = 0x5E
WEDDING_MAP_TRANSFER = 0x5F
USE_CASH_PET_FOOD = 0x60
YELLOW_CHAT = 0x61
SHOP_DISCOUNT = 0x62
CATCH_MOB = 0x63
MAKE_PLAYER_NPC = 0x64
PLAYER_NPC = 0x65
DISABLE_NPC = 0x66
GET_CARD = 0x67
CARD_SET = 0x69
BOOK_STATS = 0x6A
UPDATE_CODEX = 0x6B
CARD_DROPS = 0x6C
FAMILIAR_INFO = 0x6D
CHANGE_HOUR = 0x6F
RESET_MINIMAP = 0x70
CONSULT_UPDATE = 0x71
CLASS_UPDATE = 0x72
WEB_BOARD_UPDATE = 0x73
SESSION_VALUE = 0x74
PARTY_VALUE = 0x75
MAP_VALUE = 0x76
EXP_BONUS = 0x78
POTION_BONUS = 0x79
SEND_PEDIGREE = 0x7A
OPEN_FAMILY = 0x7B
FAMILY_MESSAGE = 0x7C
FAMILY_INVITE = 0x7D
FAMILY_JUNIOR = 0x7E
SENIOR_MESSAGE = 0x7F
FAMILY = 0x80
REP_INCREASE = 0x81
FAMILY_LOGGEDIN = 0x82
FAMILY_BUFF = 0x83
FAMILY_USE_REQUEST = 0x84
LEVEL_UPDATE = 0x85
MARRIAGE_UPDATE = 0x86
JOB_UPDATE = 0x87
MAPLE_TV_MSG = 0x89
AVATAR_MEGA_RESULT = 0x8A
AVATAR_MEGA = 0x8B
AVATAR_MEGA_REMOVE = 0x8C
CANCEL_NAME_CHANGE = 0x8D
CANCEL_WORLD_TRANSFER = 0x8E
CLOSE_HIRED_MERCHANT = 0x8F
GM_POLICE = 0x90
TREASURE_BOX = 0x91
NEW_YEAR_CARD = 0x92
RANDOM_MORPH = 0x93
CANCEL_NAME_CHANGE_2 = 0x94
PENDANT_SLOT = 0x95
FOLLOW_REQUEST = 0x96
TOP_MSG = 0x97
MID_MSG = 0x98
CLEAR_MID_MSG = 0x99
MAPLE_ADMIN_MSG = 0x9A
CAKE_VS_PIE_MSG = 0x9B
GM_STORY_BOARD = 0x9C
INVENTORY_FULL = 0x9D
UPDATE_JAGUAR = 0x9E
YOUR_INFORMATION = 0x9F
FIND_FRIEND = 0xA0
VISITOR = 0xA1
PINKBEAN_CHOCO = 0xA2
PAM_SONG = 0xA3
AUTO_CC_MSG = 0xA4
DISALLOW_DELIVERY_QUEST = 0xA5
ULTIMATE_EXPLORER = 0xA6
PROFESSION_INFO = 0xA8
UPDATE_IMP_TIME = 0xA9
ITEM_POT = 0xAA
GIVE_CHARACTER_SKILL = 0xAE
MULUNG_DOJO_RANKING = 0xB1
MULUNG_MESSAGE = 0xB4
MAGIC_WHEEL_START = 0xB6
MAGIC_WHEEL_RECEIVE = 0xB7
SKILL_MACRO = 0xB8
WARP_TO_MAP = 0xB9
MTS_OPEN = 0xBA
CS_OPEN = 0xBB
CHANGE_BACKGROUND = 0xBC
LOGIN_WELCOME = 0xBD
RESET_SCREEN = 0xBE
MAP_BLOCKED = 0xBF
SERVER_BLOCKED = 0xC0
PARTY_BLOCKED = 0xC1
SHOW_EQUIP_EFFECT = 0xC2
MULTICHAT = 0xC3
WHISPER = 0xC4
SPOUSE_CHAT = 0xC5
BOSS_ENV = 0xC7
MOVE_ENV = 0xC8
UPDATE_ENV = 0xC9
MAP_EFFECT = 0xCB
CASH_SONG = 0xCC
GM_EFFECT = 0xCD
OX_QUIZ = 0xCE
GMEVENT_INSTRUCTIONS = 0xCF
CLOCK = 0xD0
BOAT_MOVE = 0xD1
BOAT_STATE = 0xD2
SET_OBJECT_STATE = 0xD6
STOP_CLOCK = 0xD7
ARIANT_SCOREBOARD = 0xD8
PYRAMID_UPDATE = 0xDA
PYRAMID_RESULT = 0xDB
QUICK_SLOT = 0xDC
MOVE_PLATFORM = 0xDD
PYRAMID_KILL_COUNT = 0xDF
PVP_INFO = 0xE1
DIRECTION_STATUS = 0xE2
GAIN_FORCE = 0xE3
ACHIEVEMENT_RATIO = 0xE4
PUBLIC_NPC = 0xE5
SPAWN_PLAYER = 0xE6
REMOVE_PLAYER_FROM_MAP = 0xE7
CHATTEXT = 0xE8
CHATTEXT_1 = 0xE9
CHALKBOARD = 0xEA
UPDATE_CHAR_BOX = 0xEB
SHOW_CONSUME_EFFECT = 0xEC
SHOW_SCROLL_EFFECT = 0xED
SHOW_MAGNIFYING_EFFECT = 0xEF
SHOW_POTENTIAL_RESET = 0xF0
SHOW_FIREWORKS_EFFECT = 0xF1
SHOW_NEBULITE_EFFECT = 0xF2
SHOW_FUSION_EFFECT = 0xF3
PVP_ATTACK = 0xF4
PVP_MIST = 0xF5
PVP_COOL = 0xF7
TESLA_TRIANGLE = 0xF8
FOLLOW_EFFECT = 0xF9
SHOW_PQ_REWARD = 0xFA
CRAFT_EFFECT = 0xFB
CRAFT_COMPLETE = 0xFC
HARVESTED = 0xFD
PLAYER_DAMAGED = 0xFF
NETT_PYRAMID = 0x100
SET_PHASE = 0x101
PAMS_SONG = 0x103
SPAWN_PET = 0x104
SPAWN_PET_2 = 0x106
MOVE_PET = 0x107
PET_CHAT = 0x108
PET_NAMECHANGE = 0x109
PET_EXCEPTION_LIST = 0x10A
PET_COMMAND = 0x10B
DRAGON_SPAWN = 0x10C
DRAGON_MOVE = 0x10D
DRAGON_REMOVE = 0x10E
ANDROID_SPAWN = 0x10F
ANDROID_MOVE = 0x110
ANDROID_EMOTION = 0x111
ANDROID_UPDATE = 0x112
ANDROID_DEACTIVATED = 0x113
SPAWN_FAMILIAR = 0x114
MOVE_FAMILIAR = 0x115
TOUCH_FAMILIAR = 0x116
ATTACK_FAMILIAR = 0x117
RENAME_FAMILIAR = 0x118
SPAWN_FAMILIAR_2 = 0x119
UPDATE_FAMILIAR = 0x11A
MOVE_PLAYER = 0x11C
CLOSE_RANGE_ATTACK = 0x11E
RANGED_ATTACK = 0x11F
MAGIC_ATTACK = 0x120
ENERGY_ATTACK = 0x121
SKILL_EFFECT = 0x122
MOVE_ATTACK = 0x123
CANCEL_SKILL_EFFECT = 0x124
DAMAGE_PLAYER = 0x125
FACIAL_EXPRESSION = 0x126
SHOW_ITEM_EFFECT = 0x128
SHOW_TITLE = 0x12A
SHOW_CHAIR = 0x12D
UPDATE_CHAR_LOOK = 0x12E
SHOW_FOREIGN_EFFECT = 0x12F
GIVE_FOREIGN_BUFF = 0x130
CANCEL_FOREIGN_BUFF = 0x131
UPDATE_PARTYMEMBER_HP = 0x132
LOAD_GUILD_NAME = 0x133
LOAD_GUILD_ICON = 0x134
LOAD_TEAM = 0x135
SHOW_HARVEST = 0x137
PVP_HP = 0x138
CANCEL_CHAIR = 0x13B
FACIAL_EXPRESSION_2 = 0x13C
SHOW_ITEM_GAIN_INCHAT = 0x13E
CURRENT_MAP_WARP = 0x13F
MESOBAG_SUCCESS = 0x141
MESOBAG_FAILURE = 0x142
R_MESOBAG_SUCCESS = 0x143
R_MESOBAG_FAILURE = 0x144
MAP_FADE = 0x145
MAP_FADE_FORCE = 0x146
UPDATE_QUEST_INFO = 0x147
HP_DECREASE = 0x148
PLAYER_HINT = 0x14A
PLAY_EVENT_SOUND = 0x14B
PLAY_MINIGAME_SOUND = 0x14C
MAKER_SKILL = 0x14D
OPEN_UI = 0x150
OPEN_UI_OPTION = 0x152
CYGNUS_INTRO_LOCK = 0x153
CYGNUS_INTRO_ENABLE_UI = 0x154
CYGNUS_INTRO_DISABLE_UI = 0x155
SUMMON_HINT = 0x156
SUMMON_HINT_MSG = 0x157
ARAN_COMBO = 0x158
ARAN_COMBO_RECHARGE = 0x159
RANDOM_EMOTION = 0x15A
RADIO_SCHEDULE = 0x15D
OPEN_SKILL_GUIDE = 0x15E
NOTICE_MSG = 0x15F
GAME_MESSAGE = 0x160
BUFF_ZONE_EFFECT = 0x162
GO_CASHSHOP_SN = 0x163
DAMAGE_METER = 0x164
TIME_BOMB_ATTACK = 0x165
FOLLOW_MOVE = 0x166
FOLLOW_MSG = 0x167
AP_SP_EVENT = 0x169
QUEST_GUIDE_NPC = 0x16A
REGISTER_FAMILIAR = 0x171
FAMILIAR_NAME_ERROR = 0x172
CREATE_ULTIMATE = 0x173
HARVEST_MESSAGE = 0x174
SHOW_MAP_NAME = 0x175
OPEN_BAG = 0x176
DRAGON_BLINK = 0x177
PVP_ICEGAGE = 0x178
DIRECTION_INFO = 0x179
REISSUE_MEDAL = 0x17A
PLAY_MOVIE = 0x17D
CAKE_VS_PIE = 0x17F
COOLDOWN = 0x181
SPAWN_SUMMON = 0x183
REMOVE_SUMMON = 0x184
MOVE_SUMMON = 0x185
SUMMON_ATTACK = 0x186
PVP_SUMMON = 0x187
SUMMON_SKILL = 0x189
SUMMON_SKILL_2 = 0x18A
SUMMON_DELAY = 0x18B
DAMAGE_SUMMON = 0x18C
SPAWN_MONSTER = 0x18D
KILL_MONSTER = 0x18E
SPAWN_MONSTER_CONTROL = 0x18F
MONSTER_CRC_CHANGE = 0x19A
REMOVE_TALK_MONSTER = 0x1A3
MOVE_MONSTER = 0x190
MOVE_MONSTER_RESPONSE = 0x191
APPLY_MONSTER_STATUS = 0x193
CANCEL_MONSTER_STATUS = 0x194
DAMAGE_MONSTER = 0x197
SKILL_EFFECT_MOB = 0x198
SHOW_MONSTER_HP = 0x19B
SHOW_MAGNET = 0x19C
ITEM_EFFECT_MOB = 0x19D
CATCH_MONSTER = 0x19E
MONSTER_PROPERTIES = 0x1A2
TALK_MONSTER = 0x1A4
CYGNUS_ATTACK = 0x1A8
MONSTER_RESIST = 0x1A9
MOB_TO_MOB_DAMAGE = 0x1AA
SPAWN_NPC = 0x1AC
REMOVE_NPC = 0x1AD
SPAWN_NPC_REQUEST_CONTROLLER = 0x1AF
NPC_ACTION = 0x1B0
NPC_SPAWN_EFFECT = 0x1B1
NPC_SCRIPTABLE = 0x1B6
SPAWN_HIRED_MERCHANT = 0x1B8
DESTROY_HIRED_MERCHANT = 0x1B9
UPDATE_HIRED_MERCHANT = 0x1BA
DROP_ITEM_FROM_MAPOBJECT = 0x1BB
REMOVE_ITEM_FROM_MAP = 0x1BD
SPAWN_KITE_ERROR = 0x1BE
SPAWN_KITE = 0x1BF
DESTROY_KITE = 0x1C0
SPAWN_MIST = 0x1C1
REMOVE_MIST = 0x1C2
SPAWN_DOOR = 0x1C3
REMOVE_DOOR = 0x1C4
MECH_DOOR_SPAWN = 0x1C5
MECH_DOOR_REMOVE = 0x1C6
REACTOR_HIT = 0x1C7
REACTOR_MOVE = 0x1C8
REACTOR_SPAWN = 0x1C9
REACTOR_DESTROY = 0x1CB
SPAWN_EXTRACTOR = 0x1CC
REMOVE_EXTRACTOR = 0x1CD
ROLL_SNOWBALL = 0x1CE
HIT_SNOWBALL = 0x1CF
SNOWBALL_MESSAGE = 0x1D0
LEFT_KNOCK_BACK = 0x1D1
HIT_COCONUT = 0x1D2
COCONUT_SCORE = 0x1D3
MOVE_HEALER = 0x1D4
PULLEY_STATE = 0x1D5
MONSTER_CARNIVAL_START = 0x1D6
MONSTER_CARNIVAL_OBTAINED_CP = 0x1D7
MONSTER_CARNIVAL_STATS = 0x1D8
MONSTER_CARNIVAL_SUMMON = 0x1DA
MONSTER_CARNIVAL_MESSAGE = 0x1DB
MONSTER_CARNIVAL_DIED = 0x1DC
MONSTER_CARNIVAL_LEAVE = 0x1DD
MONSTER_CARNIVAL_RESULT = 0x1DE
MONSTER_CARNIVAL_RANKING = 0x1DF
ARIANT_SCORE_UPDATE = 0x1E0
SHEEP_RANCH_INFO = 0x1E2
SHEEP_RANCH_CLOTHES = 0x1E3
WITCH_TOWER = 0x1E4
EXPEDITION_CHALLENGE = 0x1E5
ZAKUM_SHRINE = 0x1E6
CHAOS_ZAKUM_SHRINE = 0x1E7
PVP_TYPE = 0x1E8
PVP_TRANSFORM = 0x1E9
PVP_DETAILS = 0x1EA
PVP_ENABLED = 0x1EB
PVP_SCORE = 0x1EC
PVP_RESULT = 0x1ED
PVP_TEAM = 0x1EE
PVP_SCOREBOARD = 0x1EF
PVP_POINTS = 0x1F1
PVP_KILLED = 0x1F2
PVP_MODE = 0x1F3
PVP_ICEKNIGHT = 0x1F4
HORNTAIL_SHRINE = 0x1FA
CAPTURE_FLAGS = 0x205
CAPTURE_POSITION = 0x206
CAPTURE_RESET = 0x207
PINK_ZAKUM_SHRINE = 0x208
NPC_TALK = 0x209
OPEN_NPC_SHOP = 0x20A
CONFIRM_SHOP_TRANSACTION = 0x20B
OPEN_STORAGE = 0x20E
MERCH_ITEM_MSG = 0x20F
MERCH_ITEM_STORE = 0x210
RPS_GAME = 0x211
MESSENGER = 0x212
PLAYER_INTERACTION = 0x213
TOURNAMENT = 0x214
TOURNAMENT_MATCH_TABLE = 0x215
TOURNAMENT_SET_PRIZE = 0x216
TOURNAMENT_UEW = 0x217
TOURNAMENT_CHARACTERS = 0x218
WEDDING_PROGRESS = 0x219
WEDDING_CEREMONY_END = 0x21A
DUEY = 0x21B
KEYMAP = 0x22E
PET_AUTO_HP = 0x230
PET_AUTO_MP = 0x231
START_TV = 0x237
REMOVE_TV = 0x238
ENABLE_TV = 0x239
CS_CHARGE_CASH = 0x21C
CS_UPDATE = 0x21D
CS_OPERATION = 0x21E
CS_EXP_PURCHASE = 0x21F
GIFT_RESULT = 0x220
CHANGE_NAME_CHECK = 0x221
CHANGE_NAME_RESPONSE = 0x222
CHAR_TRANSFER_WORLD = 0x224
GACHAPON_STAMPS = 0x225
FREE_CASH_ITEM = 0x226
CS_SURPRISE = 0x227
XMAS_SURPRISE = 0x228
ONE_A_DAY = 0x22A
NX_SPEND_GIFT = 0x22C
ALIEN_SOCKET_CREATOR = 0x247
BATTLE_RECORD_DAMAGE_INFO = 0x24A
CALCULATE_REQUEST_RESULT = 0x24B
VICIOUS_HAMMER = 0x24F
LUCKY_LOGOUT_GIFT = 0x256
BOOSTER_PACK = 0x999
GET_MTS_TOKENS = 0x999
MTS_OPERATION = 0x999
BOOSTER_FAMILIAR = 0x999
BLOCK_PORTAL = 0x7FFE
NPC_CONFIRM = 0x7FFE
# PIN_ASSIGNED = 0x7FFE
RSA_KEY = 0x7FFE
LOGIN_AUTH = 0x7FFE
PET_FLAG_CHANGE = 0x7FFE
BUFF_BAR = 0x7FFE
GAME_POLL_REPLY = 0x7FFE
GAME_POLL_QUESTION = 0x7FFE
ENGLISH_QUIZ = 0x7FFE
FISHING_BOARD_UPDATE = 0x7FFE
BOAT_EFFECT = 0x7FFE
FISHING_CAUGHT = 0xFF
SIDEKICK_OPERATION = 0x7FFE
class SendOps(IntEnum):
PONG = 0x2E
CLIENT_HELLO = 0x14
LOGIN_PASSWORD = 0x15
GUEST_LOGIN = 0x16
CHARLIST_REQUEST = 0x19
SERVERSTATUS_REQUEST = 0x1A
TOS = 0x1B #NOT NEEDED
SERVERLIST_REQUEST = 0x1F
REDISPLAY_SERVERLIST = 0x20
VIEW_ALL_CHAR = 0x21
PICK_ALL_CHAR = 0x22
VIEW_SERVERLIST = 0x23
CHAR_SELECT_NO_PIC = 0x27
CHECK_CHAR_NAME = 0x29
CREATE_CHAR = 0x2A
CREATE_ULTIMATE = 0x2C
DELETE_CHAR = 0x2D
CHAR_SELECT = 0x32
AUTH_SECOND_PASSWORD = 0x33
VIEW_REGISTER_PIC = 0x34
VIEW_SELECT_PIC = 0x35
CLIENT_START = 0x38
CLIENT_FAILED = 0x39
CLIENT_ERROR = 0x3B
ENABLE_LV50_CHAR = 0x3C
CREATE_LV50_CHAR = 0x3D
ENABLE_SPECIAL_CREATION = 0x3E
CREATE_SPECIAL_CHAR = 0x3F
PLAYER_LOGGEDIN = 0x28
CHANGE_MAP = 0x42
CHANGE_CHANNEL = 0x43
ENTER_CASH_SHOP = 0x44
ENTER_PVP = 0x45
ENTER_PVP_PARTY = 0x46
LEAVE_PVP = 0x48
MOVE_PLAYER = 0x49
CANCEL_CHAIR = 0x4B
USE_CHAIR = 0x4C
CLOSE_RANGE_ATTACK = 0x4D
RANGED_ATTACK = 0x4E
MAGIC_ATTACK = 0x4F
PASSIVE_ENERGY = 0x50
TAKE_DAMAGE = 0x52
PVP_ATTACK = 0x53
GENERAL_CHAT = 0x54
CLOSE_CHALKBOARD = 0x55
FACE_EXPRESSION = 0x56
FACE_ANDROID = 0x57
USE_ITEMEFFECT = 0x58
WHEEL_OF_FORTUNE = 0x59
USE_TITLE = 0x5A
CHANGE_SET = 0x61
MONSTER_BOOK_DROPS = 0x64
NPC_TALK = 0x66
NPC_TALK_MORE = 0x68
NPC_SHOP = 0x69
STORAGE = 0x6A
USE_HIRED_MERCHANT = 0x6B
MERCH_ITEM_STORE = 0x6C
DUEY_ACTION = 0x6D
MECH_CANCEL = 0x6E
OWL = 0x70
OWL_WARP = 0x71
ITEM_SORT = 0x73
ITEM_GATHER = 0x74
ITEM_MOVE = 0x75
MOVE_BAG = 0x76
SWITCH_BAG = 0x77
USE_ITEM = 0x79
CANCEL_ITEM_EFFECT = 0x7A
USE_SUMMON_BAG = 0x7C
PET_FOOD = 0x7D
USE_MOUNT_FOOD = 0x7E
USE_SCRIPTED_NPC_ITEM = 0x7F
USE_RECIPE = 0x80
USE_NEBULITE = 0x81
USE_ALIEN_SOCKET = 0x82
USE_ALIEN_SOCKET_RESPONSE = 0x83
USE_NEBULITE_FUSION = 0x84
USE_CASH_ITEM = 0x85
USE_CATCH_ITEM = 0x999
USE_SKILL_BOOK = 0x88
USE_OWL_MINERVA = 0x8E
USE_TELE_ROCK = 0x8F
USE_RETURN_SCROLL = 0x90
USE_UPGRADE_SCROLL = 0x91
USE_FLAG_SCROLL = 0x92
USE_EQUIP_SCROLL = 0x93
USE_POTENTIAL_SCROLL = 0x95
USE_BAG = 0x98
USE_MAGNIFY_GLASS = 0x99
DISTRIBUTE_AP = 0x9A
AUTO_ASSIGN_AP = 0x9B
HEAL_OVER_TIME = 0x9C
DISTRIBUTE_SP = 0x9F
SPECIAL_MOVE = 0xA0
CANCEL_BUFF = 0xA1
SKILL_EFFECT = 0xA2
MESO_DROP = 0xA3
GIVE_FAME = 0xA4
CHAR_INFO_REQUEST = 0xA6
SPAWN_PET = 0xA7
GET_BOOK_INFO = 0xA8
USE_FAMILIAR = 0xAA
SPAWN_FAMILIAR = 0xAB
RENAME_FAMILIAR = 0xAC
CANCEL_DEBUFF = 0xAE
CHANGE_MAP_SPECIAL = 0xAF
USE_INNER_PORTAL = 0xB1
TROCK_ADD_MAP = 0xB2
REPORT = 0x999
QUEST_ACTION = 0xB8
REISSUE_MEDAL = 0xB9
SKILL_MACRO = 0xBC
REWARD_ITEM = 0xBE
ITEM_MAKER = 0x999
REPAIR_ALL = 0xC4
REPAIR = 0xC5
SOLOMON = 0xC6
GACH_EXP = 0xC7
FOLLOW_REQUEST = 0xC8
PQ_REWARD = 0xCA
FOLLOW_REPLY = 0xCC
AUTO_FOLLOW_REPLY = 0x999
USE_TREASUER_CHEST = 0x999
PROFESSION_INFO = 0xD2
USE_POT = 0xD3
CLEAR_POT = 0xD4
FEED_POT = 0xD5
CURE_POT = 0xD6
REWARD_POT = 0xD7
USE_COSMETIC = 0x999
PVP_RESPAWN = 0xD8
GAIN_FORCE = 0xDA
PARTYCHAT = 0xDD
WHISPER = 0xDE
SPOUSE_CHAT = 0xDF
MESSENGER = 0xE0
PLAYER_INTERACTION = 0xE1
PARTY_OPERATION = 0xE2
DENY_PARTY_REQUEST = 0xE3
ALLOW_PARTY_INVITE = 0xE4
EXPEDITION_OPERATION = 0xE5
EXPEDITION_LISTING = 0xE6
GUILD_OPERATION = 0xE7
DENY_GUILD_REQUEST = 0xE8
ADMIN_COMMAND = 0xE9 #TODO ADD TO SOURCE
ADMIN_LOG = 0xEA #TODO ADD TO SOURCE
BUDDYLIST_MODIFY = 0xEB
NOTE_ACTION = 0xEC
USE_DOOR = 0xEE
USE_MECH_DOOR = 0xEF
CHANGE_KEYMAP = 0xF1
RPS_GAME = 0xF2
RING_ACTION = 0xF3
WEDDING_ACTION = 0xF4
ALLIANCE_OPERATION = 0xF8
DENY_ALLIANCE_REQUEST = 0xF9
REQUEST_FAMILY = 0xFA
OPEN_FAMILY = 0xFB
FAMILY_OPERATION = 0xFC
DELETE_JUNIOR = 0xFD
DELETE_SENIOR = 0xFE
ACCEPT_FAMILY = 0xFF
USE_FAMILY = 0x100
FAMILY_PRECEPT = 0x101
FAMILY_SUMMON = 0x102
BBS_OPERATION = 0x104
ENTER_MTS = 0x105
NEW_YEAR_CARD = 0x108
XMAS_SURPRISE = 0x10A
TWIN_DRAGON_EGG = 0x10B
ARAN_COMBO = 0x10E
TRANSFORM_PLAYER = 0x999
CYGNUS_SUMMON = 0x999
CRAFT_DONE = 0x110
CRAFT_EFFECT = 0x111
CRAFT_MAKE = 0x112
CHANGE_ROOM_CHANNEL = 0x116
EVENT_CARD = 0x117
YOUR_INFORMATION = 0x11B
FIND_FRIEND = 0x11C
PINKBEAN_CHOCO_OPEN = 0x11E
PINKBEAN_CHOCO_SUMMON = 0x11F
MOVE_PET = 0x123
PET_CHAT = 0x124
PET_COMMAND = 0x125
PET_LOOT = 0x126
PET_AUTO_POT = 0x127
PET_IGNORE = 0x128
MOVE_SUMMON = 0x12B
SUMMON_ATTACK = 0x12C
DAMAGE_SUMMON = 0x12D
SUB_SUMMON = 0x12E
REMOVE_SUMMON = 0x12F
PVP_SUMMON = 0x130
MOVE_DRAGON = 0x133
MOVE_ANDROID = 0x137
MOVE_FAMILIAR = 0x13B
TOUCH_FAMILIAR = 0x13C
ATTACK_FAMILIAR = 0x13D
REVEAL_FAMILIAR = 0x13E
QUICK_SLOT = 0x140
#SKILL_MEMORY = 0x141 INT
PAM_SONG = 0x145
MOVE_LIFE = 0x14F
AUTO_AGGRO = 0x150
FRIENDLY_DAMAGE = 0x153
MONSTER_BOMB = 0x154
HYPNOTIZE_DMG = 0x155
MOB_BOMB = 0x157
MOB_NODE = 0x158
DISPLAY_NODE = 0x159
NPC_ACTION = 0x15F
ITEM_PICKUP = 0x164
DAMAGE_REACTOR = 0x167
CLICK_REACTOR = 0x168
TOUCH_REACTOR = 0x169
MAKE_EXTRACTOR = 0x999
UPDATE_ENV = 0x16B
SNOWBALL = 0x999
LEFT_KNOCK_BACK = 0x999
COCONUT = 0x999
MONSTER_CARNIVAL = 0x17B
SHIP_OBJECT = 0x999
PARTY_SEARCH_START = 0x184
PARTY_SEARCH_STOP = 0x185
START_HARVEST = 0x189
STOP_HARVEST = 0x18A
PUBLIC_NPC = 0x18C
CS_UPDATE = 0x192
BUY_CS_ITEM = 0x193
COUPON_CODE = 0x194
UPDATE_QUEST = 0x999
QUEST_ITEM = 0x999
USE_ITEM_QUEST = 0x999
VICIOUS_HAMMER = 0x1AB
TOUCHING_MTS = 0x999
MTS_TAB = 0x999
PYRAMID_BUY_ITEM = 0x17E
CLASS_COMPETITION = 0x1A6
MAGIC_WHEEL = 0x1BD

176
packet.py Normal file

@ -0,0 +1,176 @@
from enum import Enum, IntEnum
from io import BytesIO
from struct import pack, unpack
from .opcodes import RecvOps, SendOps
from .tools import to_string
class ByteBuffer(BytesIO):
"""Base class for packet write and read operations"""
def encode(self, _bytes):
self.write(_bytes)
return self
def encode_byte(self, value):
if isinstance(value, Enum):
value = value.value
self.write(bytes([value]))
return self
def encode_short(self, value):
self.write(pack("H", value))
return self
def encode_int(self, value):
self.write(pack("I", value))
return self
def encode_long(self, value):
self.write(pack("Q", value))
return self
def encode_buffer(self, buffer):
self.write(buffer)
return self
def skip(self, count):
self.write(bytes(count))
return self
def encode_string(self, string):
self.write(pack("H", len(string)))
for ch in string:
self.write(ch.encode())
return self
def encode_fixed_string(self, string, length=13):
for i in range(length):
if i < len(string):
self.write(string[i].encode())
continue
self.encode_byte(0)
return self
def encode_hex_string(self, string):
string = string.strip(" -")
self.write(bytes.fromhex(string))
return self
def decode_byte(self):
return self.read(1)[0]
def decode_bool(self):
return bool(self.decode_byte())
def decode_short(self):
return unpack("H", self.read(2))[0]
def decode_int(self):
return unpack("I", self.read(4))[0]
def decode_long(self):
return unpack("Q", self.read(8))[0]
def decode_buffer(self, size):
return self.read(size)
def decode_string(self):
length = self.decode_short()
string = ""
for _ in range(length):
string += self.read(1).decode()
return string
class Packet(ByteBuffer):
"""Packet class use in all send / recv opertions
Parameters
----------
data: bytes
The initial data to load into the packet
op_code: :class:`OpCodes`
OpCode used to encode the first short onto the packet
op_codes: :class:`OpCodes`
Which enum to try to get the op_code from
"""
_op_codes: type[IntEnum]
def __init__(self, data=None, op_code=None, raw=False):
self.op_code: IntEnum | int
if not data:
data = bytearray()
if isinstance(data, IntEnum):
if op_code and isinstance(op_code, bytearray):
op_code, data = data, op_code
else:
op_code, data = data, bytearray()
super().__init__(data)
@property
def name(self):
if isinstance(self.op_code, IntEnum):
return self.op_code.name
return self.op_code
def to_array(self):
return self.getvalue()
def to_string(self):
return to_string(self.getvalue())
def __len__(self):
return len(self.getvalue())
@property
def length(self):
return len(self.getvalue())
class iPacket(Packet):
_op_codes = RecvOps
def __init__(self, data=None):
super().__init__(data=data)
self.op_code = self._op_codes(self.decode_short())
class oPacket(Packet):
_op_codes = SendOps
def __init__(self, op_code=None):
super().__init__(op_code=op_code)
self.op_code = op_code.value if hasattr(op_code, "value") else op_code
if self.op_code:
self.encode_short(self.op_code)
class PacketHandler:
def __init__(self, name, callback, op_code=None, **kwargs):
self.name = name
self.callback = callback
self.op_code = op_code
def packet_handler(op_code=None):
def wrap(func):
return PacketHandler(func.__name__, func, op_code=op_code)
return wrap

125
tools.py Normal file

@ -0,0 +1,125 @@
from asyncio import sleep
from dataclasses import dataclass, is_dataclass
from random import randint
class TagPoint:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return f"{self.x},{self.y}"
class Random:
def __init__(self):
self.seed_1 = randint(1, 2**31 - 1)
self.seed_2 = randint(1, 2**31 - 1)
self.seed_3 = randint(1, 2**31 - 1)
def encode(self, packet):
packet.encode_int(self.seed_1)
packet.encode_int(self.seed_2)
packet.encode_int(self.seed_3)
def find(predicate, seq):
for element in seq:
if predicate(element):
return element
return None
def get(iterable, **attrs):
def predicate(elem):
for attr, val in attrs.items():
nested = attr.split("__")
obj = elem
for attribute in nested:
obj = getattr(obj, attribute)
if obj != val:
return False
return True
return find(predicate, iterable)
def filter_out_to(func, iters, out):
new = []
for item in iters:
if func(item):
new.append(item)
else:
out.append(item)
return new
def first_or_default(list_, f):
return next((val for val in list_ if f(val)), None)
def fix_dict_keys(dict_):
copy = dict(dict_)
for key, value in copy.items():
if key.isdigit():
value = dict_.pop(key)
key = int(key)
if isinstance(value, dict):
dict_[key] = fix_dict_keys(value)
else:
dict_[key] = value
return dict_
def to_string(bytes_):
return " ".join([
bytes_.hex()[i:i + 2].upper() for i in range(0, len(bytes_.hex()), 2)
])
async def wakeup():
while True:
await sleep(0.01)
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__
def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
original_init(self, *args, **kwargs)
cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper
class Manager(list):
def get(self, search):
return first_or_default(self, search)
def first_or_default(self, func):
return next((val for val in self if func(val)), None)