large amount of restructuring

This commit is contained in:
Ra 2021-12-08 13:17:50 -08:00
parent c377749789
commit ea650019d4
102 changed files with 2514 additions and 2213 deletions

6
.gitignore vendored
View File

@ -1,7 +1,7 @@
.vscode/
.vscode
*.pyc
__pycache__/
__pycache__
.mypy_cache/
config.ini
scratch.md
test.py
test*.py

View File

@ -5,8 +5,9 @@ Maplestory emulator written in Python
## Requirements
- Postgresql (only tested with 11)
- Python 3.8+
- Python 3.10+
- asyncpg
- pycryptodomex
## To-Do

View File

@ -1,22 +0,0 @@
from dataclasses import dataclass, field
from typing import List
from common.abc import WildcardData
from common.constants import PERMANENT
def default_level_data():
return []
@dataclass
class SkillEntry(WildcardData):
id: int = 0
level: int = 0
mastery_level: int = 0
max_level: int = 0
expiration: int = field(default=PERMANENT)
level_data: List = field(default_factory=default_level_data)
def encode(self, packet):
packet.encode_int(self.id)
packet.encode_int(self.level)
packet.encode_long(PERMANENT) # skill.expiration

View File

@ -1,85 +0,0 @@
import server
from net.packets.packet import Packet
from net.packets.crypto import MapleIV
from net.client.client_socket import ClientSocket
from common.constants import VERSION, SUB_VERSION, LOCALE
from asyncio import create_task
from random import randint
from time import time
from utils import log
class ClientBase:
def __init__(self, parent, socket):
self.m_socket = socket
self._parent = parent
self.port = None
self._is_alive = False
self.logged_in = False
self.world_id = None
self.channel_id = None
async def initialize(self):
self.m_socket.m_siv = MapleIV(randint(0, 2**31-1))
# self.m_socket.m_siv = MapleIV(100)
self.m_socket.m_riv = MapleIV(randint(0, 2**31-1))
# self.m_socket.m_riv = MapleIV(50)
packet = Packet(op_code=0x0E)
packet.encode_short(VERSION)
packet.encode_string(SUB_VERSION)
packet.encode_int(self.m_socket.m_riv.value)
packet.encode_int(self.m_socket.m_siv.value)
packet.encode_byte(LOCALE)
await self.send_packet_raw(packet)
await self.m_socket.receive(self)
# async def receive(self):
# self._is_alive = True
# while self._is_alive:
# m_recv_buffer = await self.sock_recv()
# if not m_recv_buffer:
# self._parent.on_client_disconnect(self)
# return
# if self.m_socket.m_riv:
# m_recv_buffer = self.manipulate_buffer(m_recv_buffer)
# self.dispatch(Packet(m_recv_buffer))
def dispatch(self, packet):
self._parent.dispatcher.push(self, packet)
# async def sock_recv(self):
# return await self.m_socket.sock_recv()
async def send_packet(self, packet):
log.packet(f"{packet.name} {self.ip} {packet.to_string()}", "out")
await self.m_socket.send_packet(packet)
async def send_packet_raw(self, packet):
log.packet(f"{packet.name} {self.ip} {packet.to_string()}", "out")
await self.m_socket.send_packet_raw(packet)
# def manipulate_buffer(self, buffer):
# return self.m_socket.manipulate_buffer(buffer)
@property
def parent(self):
return self._parent
@property
def ip(self):
return self.m_socket.identifier[0]
@property
def data(self):
return self._parent.data

View File

View File

@ -1,74 +0,0 @@
from enum import Enum
class Worlds(Enum):
Scania = 0
Broa = 1
Windia = 2
Khaini = 3
Bellocan = 4
Mardia = 5
Kradia = 6
Yellonde = 7
Galacia = 8
El_Nido = 9
Zenith = 11
Arcenia = 12
Judis = 13
Plana = 14
Kastia = 15
Kalluna = 16
Stius = 17
Croa = 18
Medere = 19
class WorldFlag(Enum):
Null = 0x00
Event = 0x01
New = 0x02
Hot = 0x03
class InventoryType(Enum):
TRACKER = 0x0
EQUIP = 0x1
CONSUME = 0x2
INSTALL = 0x3
ETC = 0x4
CASH = 0x5
class StatModifiers(int, Enum):
def __new__(cls, value, encode_type):
obj = int.__new__(cls, value)
obj._value_ = value
obj.encode = encode_type
return obj
SKIN = (0x1, 'byte')
FACE = (0x2, 'int')
HAIR = (0x4, 'int')
PET = (0x8, 'long')
PET2 = (0x80000, 'long')
PET3 = (0x100000, 'long')
LEVEL = (0x10, 'byte')
JOB = (0x20, 'short')
STR = (0x40, 'short')
DEX = (0x80, 'short')
INT = (0x100, 'short')
LUK = (0x200, 'short')
HP = (0x400, 'int')
MAX_HP = (0x800, 'int')
MP = (0x1000, 'int')
MAX_MP = (0x2000, 'int')
AP = (0x4000, 'short')
SP = (0x8000, 'short')
EXP = (0x10000, 'int')
POP = (0x20000, 'short')
MONEY = (0x40000, 'int')
# TEMP_EXP = 0x200000

View File

@ -1 +0,0 @@
from .db_client import DatabaseClient, Account

View File

@ -1,617 +0,0 @@
from enum import Enum
class Meta(Enum):
def __str__(self):
return f"{self.__class__.__name__.lower()}.{super().__str__()}"
class Schema(Meta):
def __new__(cls, value):
value.schema = cls.__name__.lower()
value.primary_key = value.__dict__.get('__primary_key__')
value.foreign_keys = value.__dict__.get('__foreign_keys__')
value.columns = [item.lower() for item in value._member_names_]
return value
def __str__(self):
return f"{self.__class__.__name__.lower()}.{super().__str__()}"
def __getattr__(self, name):
if name not in ('_value_'):
return getattr(self.value, name)
return super().__getattr__(name)
@classmethod
def create(cls):
# async def create(db):
# schema = db.schema(cls.__name__.lower())
# await schema.create(skip_if_exists=False)
# fks = []
# for table in cls:
# t = db.table(table._name_.lower(), schema=schema)
# if await t.exists(): continue
# cols = []
# for c in table:
# primary_key = c.getattr('primary_key', False)
# col = Column(c._name_.lower(), c.data_type, primary_key=primary_key, **c.options)
# if t.foreign_keys and c.name in getattr(table, 'foreign_keys', []):
# fks.append(types.ForeignKey(t, col, sql_type=c.data_type))
# cols.append(col)
# await t.create(*cols)
# for fkey in fks:
# await db.execute_transaction(fk.to_sql())
# return create
pass
class Table(int, Meta):
_ignore_ = ('data_type')
def __new__(cls, data_type, options=None):
value = len(cls.__members__) + 1
enum_class = super().__new__(cls, value)
enum_class._value_ = value
enum_class.data_type = data_type
# if data_type in ['integer', 'serial']:
# enum_class.data_type = types.Integer(auto_increment=data_type == 'serial')
# elif data_type == 'smallint':
# enum_class.data_type = types.Integer(small=True)
# elif data_type == 'bigint':
# enum_class.data_type = types.Integer(big=True)
# elif data_type in ['varchar', 'text']:
# enum_class.data_type = types.String()
# elif data_type == 'jsonb':
# enum_class.data_type = types.JSON()
# elif data_type == 'double':
# enum_class.data_type = types.Double()
# elif data_type == 'boolean':
# enum_class.data_type = types.Boolean()
# elif data_type == 'date':
# enum_class.data_type = types.Date()
# from re import search
# if search(r'(\[\])', data_type):
# enum_class.data_type = types.ArraySQL(enum_class.data_type)
enum_class.options = options if options else {}
return enum_class
class ItemConsumeableData(Table):
ITEM_ID = ("integer")
FLAGS = ("varchar[]")
CURES = ("varchar[]")
HP = ("smallint")
HP_PERCENTAGE = ("smallint")
MAX_HP_PERCENTAGE = ("smallint")
MP = ("smallint")
MP_PERCENTAGE = ("smallint")
MAX_MP_PERCENTAGE = ("smallint")
WEAPON_ATTACK = ("smallint")
WEAPON_ATTACK_PERCENTAGE = ("smallint")
WEAPON_DEFENSE = ("smallint")
WEAPON_DEFENSE_PERCENTAGE = ("smallint")
MAGIC_ATTACK = ("smallint")
MAGIC_ATTACK_PERCENTAGE = ("smallint")
MAGIC_DEFENSE = ("smallint")
MAGIC_DEFENSE_PERCENTAGE = ("smallint")
ACCURACY = ("smallint")
ACCURACY_PERCENTAGE = ("smallint")
AVOID = ("smallint")
AVOID_PERCENTAGE = ("smallint")
SPEED = ("smallint")
SPEED_PERCENTAGE = ("smallint")
JUMP = ("smallint")
SECONDARY_STAT = ("jsonb")
BUFF_TIME = ("integer")
PROB = ("smallint")
EVENT_POINT = ("integer")
MOB_ID = ("integer")
MOB_HP = ("integer")
SCREEN_MESSAGE = ("text")
ATTACK_INDEX = ("smallint")
ATTACK_MOB_ID = ("integer")
MOVE_TO = ("integer")
RETURN_MAP_QR = ("integer")
DECREASE_HUNGER = ("smallint")
MORPH = ("smallint")
CARNIVAL_TYPE = ("smallint")
CARNIVAL_POINTS = ("smallint")
CARNIVAL_SKILL = ("smallint")
EXPERIENCE = ("integer")
__primary_key__ = ("ITEM_ID",)
class ItemData(Table):
ITEM_ID = ("integer")
INVENTORY = ("smallint")
PRICE = ("integer", {"default": 0})
MAX_SLOT_QUANTITY = ("smallint")
MAX_POSSESSION_COUNT = ("smallint")
MIN_LEVEL = ("smallint")
MAX_LEVEL = ("smallint", {"default": 250})
EXPERIENCE = ("integer")
MONEY = ("integer")
STATE_CHANGE_ITEM = ("integer")
LEVEL_FOR_MAKER = ("smallint")
NPC = ("integer")
FLAGS = ("varchar[]", {"default": "'{}'"})
PET_LIFE_EXTEND = ("smallint")
MAPLE_POINT = ("integer")
MONEY_MIN = ("integer")
MONEY_MAX = ("integer")
EXP_RATE = ("double")
ADD_TIME = ("smallint")
SLOT_INDEX = ("smallint")
__primary_key__ = ("ITEM_ID",)
class ItemEquipData(Table):
ITEM_ID = ("integer")
FLAGS = ("varchar[]", {"default": "'{}'"})
EQUIP_SLOTS = ("varchar[]", {"default": "'{}'"})
ATTACK_SPEED = ("smallint", {"default": 0})
RUC = ("smallint", {"default": 0})
REQ_STR = ("smallint", {"default": 0})
REQ_DEX = ("smallint", {"default": 0})
REQ_INT = ("smallint", {"default": 0})
REQ_LUK = ("smallint", {"default": 0})
REQ_FAME = ("smallint", {"default": 0})
REQ_JOB = ("smallint[]", {"default": "'{}'"})
HP = ("smallint", {"default": 0})
HP_PERCENTAGE = ("smallint", {"default": 0})
MP = ("smallint", {"default": 0})
MP_PERCENTAGE = ("smallint", {"default": 0})
STR = ("smallint", {"default": 0})
DEX = ("smallint", {"default": 0})
INT = ("smallint", {"default": 0})
LUK = ("smallint", {"default": 0})
HANDS = ("smallint", {"default": 0})
WEAPON_ATTACK = ("smallint", {"default": 0})
WEAPON_DEFENSE = ("smallint", {"default": 0})
MAGIC_ATTACK = ("smallint", {"default": 0})
MAGIC_DEFENSE = ("smallint", {"default": 0})
ACCURACY = ("smallint", {"default": 0})
AVOID = ("smallint", {"default": 0})
JUMP = ("smallint", {"default": 0})
SPEED = ("smallint", {"default": 0})
TRACTION = ("double", {"default": 0})
RECOVERY = ("double", {"default": 0})
KNOCKBACK = ("smallint", {"default": 0})
TAMING_MOB = ("smallint", {"default": 0})
DURABILITY = ("integer", {"default": "'-1'::integer"})
INC_LIGHTNING_DAMAGE = ("smallint", {"default": 0})
INC_ICE_DAMAGE = ("smallint", {"default": 0})
INC_FIRE_DAMAGE = ("smallint", {"default": 0})
INC_POISON_DAMAGE = ("smallint", {"default": 0})
ELEMENTAL_DEFAULT = ("smallint", {"default": 0})
CRAFT = ("smallint", {"default": 0})
SET_ID = ("integer", {"default": 0})
ENCHANT_CATEGORY = ("smallint", {"default": 0})
HEAL_HP = ("smallint", {"default": 0})
SPECIAL_ID = ("integer", {"default": 0})
__primary_key__ = ("ITEM_ID")
class ItemPetData(Table):
ITEM_ID = ("integer")
HUNGER = ("smallint")
LIFE = ("smallint")
LIMITED_LIFE = ("integer")
EVOLUTION_ITEM = ("integer")
EVOLUTION_LEVEL = ("smallint")
FLAGS = ("varchar[]")
__primary_key__ = ("ITEM_ID",)
class ItemRechargeableData(Table):
ITEM_ID = ("integer")
UNIT_PRICE = ("smallint")
WEAPON_ATTACK = ("smallint")
__primary_key__ = ("ITEM_ID")
class MapData(Table):
MAP_ID = ("integer")
MAP_NAME = ("text", {"default": "''::text"})
STREET_NAME = ("text", {"default": "''::text"})
MAP_MARK = ("text", {"default": "''::text"})
FLAGS = ("varchar[]", {"default": "'{}'"})
MOB_RATE = ("double", {"default": 1.0})
FIXED_MOB_CAPACITY = ("smallint")
SPAWN_MOB_INTERVAL = ("smallint")
DROP_RATE = ("double", {"default": 1.0})
REGEN_RATE = ("double", {"default": 1.0})
SHUFFLE_NAME = ("text", {"default": "NULL::varchar"})
DEFAULT_BGM = ("text", {"default": "NULL::varchar"})
EFFECT = ("text", {"default": "NULL::varchar"})
MIN_LEVEL_LIMIT = ("smallint")
MAX_LEVEL_LIMIT = ("smallint")
TIME_LIMIT = ("smallint")
DEFAULT_TRACTION = ("double", {"default": 1})
MAP_LTX = ("smallint")
MAP_LTY = ("smallint")
MAP_RBX = ("smallint")
MAP_RBY = ("smallint")
LP_BOTTOM = ("smallint")
LP_TOP = ("smallint")
LP_SIDE = ("smallint")
FORCED_MAP_RETURN = ("integer")
FIELD_TYPE = ("smallint")
DECREASE_HP = ("integer")
DECREASE_MP = ("integer")
DECREASE_INTERVAL = ("smallint")
DAMAGE_PER_SECOND = ("smallint")
PROTECT_ITEM = ("integer")
SHIP_KIND = ("smallint")
CONSUME_COOLDOWN = ("smallint")
LINK = ("integer")
FIELD_LIMITATIONS = ("bigint", {"default": 0})
RETURN_MAP = ("integer")
MAP_LIMIT = ("smallint")
MAP_VERSION = ("smallint")
ON_FIRST_USER_ENTER = ("text")
ON_USER_ENTER = ("text")
DESCRIPTION = ("text", {"default": "''::text"})
MOVE_LIMIT = ("smallint")
PROTECT_SET = ("integer")
ALLOWED_ITEM_DROP = ("integer")
DROP_EXPIRE = ("smallint")
TIME_OUT_LIMIT = ("smallint")
PHASE_ALPHA = ("smallint")
PHASE_BACKGROUND = ("smallint")
TIME_MOB_SPAWN = ("smallint")
__primary_key__ = ("MAP_ID")
class MapFootholds(Table):
MAP_ID = ("integer")
GROUP_ID = ("smallint")
ID = ("smallint")
X_1 = ("smallint")
Y_1 = ("smallint")
X_2 = ("smallint")
Y_2 = ("smallint")
DRAG_FORCE = ("smallint")
PREVIOUS_ID = ("smallint")
NEXT_ID = ("smallint")
FLAGS = ("varchar[]")
__primary_key__ = ("MAP_ID", "GROUP_ID", "ID")
class MapLife(Table):
MAP_ID = ("integer")
LIFE_ID = ("integer")
LIFE_TYPE = ("text")
LIFE_NAME = ("text", {"default": "''::text"})
X_POS = ("smallint")
Y_POS = ("smallint")
FOOTHOLD = ("smallint")
MIN_CLICK_POS = ("smallint")
MAX_CLICK_POS = ("smallint")
RESPAWN_TIME = ("integer")
FLAGS = ("varchar[]", {"default": "'{}'"})
CY = ("smallint")
FACE = ("boolean", {"default": 0})
HIDE = ("boolean", {"default": 0})
__primary_key__ = None
class MapPortals(Table):
MAP_ID = ("integer")
ID = ("smallint")
NAME = ("text")
X_POS = ("smallint")
Y_POS = ("smallint")
DESTINATION = ("integer")
DESTINATION_LABEL = ("text")
PORTAL_TYPE = ("smallint")
__primary_key__ = ("MAP_ID", "ID")
class MapReactors(Table):
MAP_ID = ("integer")
REACTOR_ID = ("integer")
REACTOR_TIME = ("integer")
NAME = ("text")
X = ("smallint")
Y = ("smallint")
F = ("smallint")
__primary_key__ = ("MAP_ID", "REACTOR_ID")
class MapTimedMobs(Table):
MAP_ID = ("integer")
MOB_ID = ("integer")
START_HOUR = ("smallint")
END_HOUR = ("smallint")
MESSAGE = ("text")
__primary_key__ = ("MAP_ID", "MOB_ID")
class MobData(Table):
MOB_ID = ("integer")
MOB_LEVEL = ("smallint")
FLAGS = ("varchar[]", {"default": "'{}'"})
HP = ("integer")
HP_RECOVERY = ("integer")
MP = ("integer")
MP_RECOVERY = ("integer")
EXPERIENCE = ("integer")
KNOCKBACK = ("integer")
FIXED_DAMAGE = ("integer")
EXPLODE_HP = ("integer")
LINK = ("integer")
SUMMON_TYPE = ("smallint")
DEATH_BUFF = ("integer")
DEATH_AFTER = ("integer")
TRACTION = ("double", {"default": 1.0})
DAMAGED_BY_SKILL_ONLY = ("smallint")
DAMAGED_BY_MOB_ONLY = ("integer")
DROP_ITEM_PERIOD = ("smallint")
HP_BAR_COLOR = ("smallint")
HP_BAR_BG_COLOR = ("smallint")
CARNIVAL_POINTS = ("smallint")
PHYSICAL_ATTACK = ("smallint")
PHYSICAL_DEFENSE = ("smallint")
PHYSICAL_DEFENSE_RATE = ("smallint")
MAGICAL_ATTACK = ("smallint")
MAGICAL_DEFENSE = ("smallint")
MAGICAL_DEFENSE_RATE = ("smallint")
ACCURACY = ("smallint")
AVOID = ("smallint")
SPEED = ("smallint")
FLY_SPEED = ("smallint")
CHASE_SPEED = ("smallint")
ICE_MODIFIER = ("smallint")
FIRE_MODIFIER = ("smallint")
POISON_MODIFIER = ("smallint")
LIGHTNING_MODIFIER = ("smallint")
HOLY_MODIFIER = ("smallint")
DARK_MODIFIER = ("smallint")
NONELEMENTAL_MODIFIER = ("smallint")
__primary_key__ = ("MOB_ID")
class MobSkills(Table):
MOB_ID = ("integer")
SKILL_ID = ("smallint")
LEVEL = ("smallint")
EFFECT_AFTER = ("smallint")
__primary_key__ = ("MOB_ID", "SKILL_ID")
class PlayerSkillData(Table):
SKILL_ID = ("integer")
FLAGS = ("varchar[]")
WEAPON = ("smallint")
SUB_WEAPON = ("smallint")
MAX_LEVEL = ("smallint")
BASE_MAX_LEVEL = ("smallint")
SKILL_TYPE = ("varchar[]")
ELEMENT = ("text")
MOB_COUNT = ("text")
HIT_COUNT = ("text")
BUFF_TIME = ("text")
HP_COST = ("text")
MP_COST = ("text")
DAMAGE = ("text")
FIXED_DAMAGE = ("text")
CRITICAL_DAMAGE = ("text")
MASTERY = ("text")
OPTIONAL_ITEM_COST = ("smallint")
ITEM_COST = ("integer")
ITEM_COUNT = ("smallint")
BULLET_COST = ("smallint")
MONEY_COST = ("text")
X_PROPERTY = ("text")
Y_PROPERTY = ("text")
SPEED = ("text")
JUMP = ("text")
STR = ("text")
WEAPON_ATTACK = ("text")
WEAPON_DEFENSE = ("text")
MAGIC_ATTACK = ("text")
MAGIC_DEFENSE = ("text")
ACCURACY = ("text")
AVOID = ("text")
HP = ("text")
MP = ("text")
PROBABILITY = ("text")
LTX = ("smallint")
LTY = ("smallint")
RBX = ("smallint")
RBY = ("smallint")
COOLDOWN_TIME = ("text")
AVOID_CHANCE = ("text")
RANGE = ("text")
MORPH = ("smallint")
__primary_key__ = ("SKILL_ID")
class PlayerSkillRequirementData(Table):
SKILL_ID = ("integer")
REQ_SKILL_ID = ("integer")
REQ_LEVEL = ("smallint")
__primary_key__ = ("SKILL_ID", "REQ_SKILL_ID")
class String(Table):
OBJECT_ID = ("integer")
OBJECT_TYPE = ("varchar")
DESCRIPTION = ("text")
LABEL = ("text")
__primary_key__ = ("OBJECT_ID", "OBJECT_TYPE")
class RMDB(Schema):
ITEM_CONSUMEABLE_DATA = ItemConsumeableData
ITEM_DATA = ItemData
ITEM_EQUIP_DATA = ItemEquipData
ITEM_PET_DATA = ItemPetData
ITEM_RECHARGEABLE_DATA = ItemRechargeableData
MAP_DATA = MapData
MAP_FOOTHOLDS = MapFootholds
MAP_LIFE = MapLife
MAP_PORTALS = MapPortals
MAP_REACTORS = MapReactors
MAP_TIMED_MOBS = MapTimedMobs
MOB_DATA = MobData
MOB_SKILLS = MobSkills
PLAYER_SKILL_DATA = PlayerSkillData
PLAYER_SKILL_REQS_DATA = PlayerSkillRequirementData
STRING = String
class Account(Table):
ID = ("serial")
USERNAME = ("text")
PASSWORD = ("text")
SALT = ("text")
CREATION = ("date", {"default": "CURRENT_DATE"})
LAST_LOGIN = ("date", {"default": '"1970-01-01"::date'})
LAST_IP = ("inet", {"default": '"0.0.0.0"::inet'})
BAN = ("bool", {"default": 0})
ADMIN = ("bool", {"default": 0})
GENDER = ("bool")
__primary_key__ = ("ID",)
# __unqiue_index__ = ("USERNAME")
class Buddies(Table):
pass
class Character(Table):
ID = ("integer")
ACCOUNT_ID = ("integer")
NAME = ("text")
GENDER = ("bool")
SKIN = ("smallint")
FACE = ("smallint")
HAIR = ("smallint")
PET_LOCKER = ("int[]")
LEVEL = ("smallint")
JOB = ("smallint")
STR = ("smallint")
DEX = ("smallint")
INT = ("smallint")
LUK = ("smallint")
HP = ("smallint")
MAX_HP = ("smallint")
MP = ("smallint")
MAX_MP = ("smallint")
AP = ("smallint")
SP = ("smallint")
EXP = ("integer")
MONEY = ("integer")
TEMP_EXP = ("integer")
FIELD_ID = ("integer")
PORTAL = ("text")
PLAY_TIME = ("integer")
SUB_JOB = ("smallint")
FAME = ("smallint")
EXTEND_SP = ("smallint[]")
WORLD_ID = ("smallint")
__primary_key__ = ("ID",)
__foreign_keys__ = ({"account_id": "accounts.id"})
class InventoryEquipment(Table):
INVENTORY_ITEM_ID = ("bigint")
UPGRADE_SLOTS = ("integer")
STR = ("smallint")
DEX = ("smallint")
INT = ("smallint")
LUK = ("smallint")
HP = ("integer")
MP = ("integer")
WEAPON_ATTACK = ("smallint")
MAGIC_ATTACK = ("smallint")
WEAPON_DEFENSE = ("smallint")
MAGIC_DEFENSE = ("smallint")
ACCURACY = ("smallint")
AVOID = ("smallint")
HANDS = ("smallint")
SPEED = ("smallint")
JUMP = ("smallint")
RING_ID = ("integer")
CRAFT = ("smallint")
ATTRIBUTE = ("smallint")
LEVEL_UP_TYPE = ("smallint")
LEVEL = ("smallint")
IUC = ("integer")
GRADE = ("smallint")
EXP = ("smallint")
CHUC = ("smallint")
OPTION_1 = ("smallint")
OPTION_2 = ("smallint")
OPTION_3 = ("smallint")
SOCKET_1 = ("smallint")
SOCKET_2 = ("smallint")
LISN = ("bigint")
CUC = ("smallint")
RUC = ("smallint")
__primary_key__ = ("inventory_item_id")
__foreign_keys__ = {"INVENTORY_ITEM_ID": "inventory_items.inventory_item_id"}
class InventoryItems(Table):
INVENTORY_ITEM_ID = ("bigint")
CHARACTER_ID = ("integer")
STORAGE_ID = ("integer")
ITEM_ID = ("integer")
INVENTORY_TYPE = ("smallint")
POSITION = ("integer")
OWNER = ("text")
PET_ID = ("bigint")
QUANTITY = ("integer")
CISN = ("bigint") # Current inventory Serial
SN = ("bigint") # Serial Number (Would replace inventory_item_id?)
EXPIRE = ("bigint")
__primary_key__ = ("inventory_item_id")
# class InventoryPets(Table):
# pass
# class Keymap(Table):
# pass
# class Skills(Table):
# pass
class Maplestory(Schema):
ACCOUNTS = Account
BUDDIES = Buddies
CHARACTERS = Character
INVENTORY_EQUIPMENT = InventoryEquipment
INVENTORY_ITEMS = InventoryItems
# INVENTORY_PETS = InventoryPets
# KEYMAP = Keymap
# SKILLS = Skills

View File

@ -1,6 +0,0 @@
from .field import Field
from .field_object import FieldObject
from .foothold import Foothold
from .mob import Mob
from .npc import Npc
from .portal import Portal

View File

@ -1,32 +0,0 @@
from dataclasses import dataclass
from common.abc import WildcardData
from .move_path import MovePath
from utils import log
class FieldObject(WildcardData):
def __init__(self):
self._obj_id = -1
self._position = MovePath()
self._field = None
@property
def obj_id(self):
return self._obj_id
@obj_id.setter
def obj_id(self, value):
self._obj_id = value
@property
def position(self):
return self._position
@property
def field(self):
return self._field
@field.setter
def field(self, value):
self._field = value

View File

@ -1,25 +0,0 @@
from dataclasses import dataclass
from common.abc import WildcardData
@dataclass
class Foothold(WildcardData):
id: int = 0
prev: int = 0
next: int = 0
x1: int = 0
y1: int = 0
x2: int = 0
y2: int = 0
@property
def wall(self):
return self.x1 == self.x2
def compare_to(self, foothold):
if self.y2 < foothold.y1:
return -1
if self.y1 > foothold.y2:
return 1
return 0

View File

@ -1,18 +0,0 @@
from dataclasses import dataclass
from .field_object import FieldObject
@dataclass
class Life(FieldObject):
life_id: int = 0
life_type: str = ""
foothold: int = 0
x: int = 0
y: int = 0
cy: int = 0
f: int = 0
hide: int = 0
rx0: int = 0 # min click position
rx1: int = 0 # max click position
mob_time: int = 0

View File

@ -1,51 +0,0 @@
from dataclasses import dataclass
from .life import Life
from .move_path import MovePath
# from utils import TagPoint
@dataclass
class Mob(Life):
mob_id: int = 0
hp: int = 0
mp: int = 0
hp_recovery: int = 0
mp_recovery: int = 0
exp: int = 0
physical_attack: int = 0
def __post_init__(self):
self.attackers = {}
self.pos = MovePath(self.x, self.cy, self.foothold)
self.cur_hp = self.hp
self.cur_mp = self.mp
self.controller = 0
@property
def dead(self):
return self.cur_hp <= 0
def damage(self, character, amount):
pass
def encode_init(self, packet):
packet.encode_int(self.obj_id)
packet.encode_byte(5)
packet.encode_int(self.life_id)
# Set Temporary Stat
packet.encode_long(0)
packet.encode_long(0)
packet.encode_short(self.pos.x)
packet.encode_short(self.pos.y)
packet.encode_byte(0 & 1 | 2 * 2)
packet.encode_short(self.pos.foothold)
packet.encode_short(self.pos.foothold)
packet.encode_byte(abs(-2))
packet.encode_byte(0)
packet.encode_int(0)
packet.encode_int(0)

View File

@ -1,28 +0,0 @@
from .object_pool import ObjectPool
class MobPool(ObjectPool):
def __init__(self, field):
super().__init__(field)
self.spawns = []
def add(self, mob):
mob.field = self.field
super().add(mob)
self.spawns.append(mob)
async def remove(self, key):
mob = self.get(key)
if mob:
owner = None
# await self.field.broadcast(CPacket.mob_leave_field(mob))
if mob.dead:
pass
# drop_pos_x = mob.position.x
# drop_pos_y = mob.position.y
# self.field.drops.add(Drop(0, mob.position, 50, 1))
return super().remove(key)

View File

@ -1,11 +0,0 @@
from dataclasses import dataclass
from .life import Life
from .move_path import MovePath
@dataclass
class Npc(Life):
def __post_init__(self):
self.pos = MovePath(self.x, self.cy, self.foothold)
self.id = self.life_id

View File

@ -1,5 +0,0 @@
from .object_pool import ObjectPool
class NpcPool(ObjectPool):
pass

View File

@ -1,32 +0,0 @@
class ObjectPool:
def __init__(self, field):
self.field = field
self.cache = {}
self.uid_base = 1000
@property
def new_uid(self):
self.uid_base += 1
return self.uid_base
def add(self, value):
value.obj_id = self.new_uid
self.cache[value.obj_id] = value
def remove(self, key):
return self.cache.pop(key)
def clear(self):
self.cache = {}
def get(self, key):
return self.cache.get(key, None)
def __enumerator__(self):
return (obj for obj in self.cache.values())
def __iter__(self):
return (obj for obj in self.cache.values())
def __aiter__(self):
return self.__iter__()

View File

@ -1,21 +0,0 @@
from dataclasses import dataclass
from common.abc import WildcardData
from utils import TagPoint
@dataclass
class Portal(WildcardData):
id: int = 0
name: str = ""
type: int = 0
destination: int = 0
destination_label: str = ""
x: int = 0
y: int = 0
def __post_init__(self):
self.point = TagPoint(self.x, self.y)
def __str__(self):
return f"{id} @ {self.point} -> {self.destination}"

View File

@ -1,14 +0,0 @@
from .object_pool import ObjectPool
class UserPool(ObjectPool):
def add(self, client):
super().add(client)
client.character.field = self.field
@property
def characters(self):
return [client.character for client in self]
def __aiter__(self):
return [client for client in self]

View File

@ -1,116 +0,0 @@
from aiohttp import ClientSession
from loguru import logger
from character import Character
from game import ItemSlotEquip
from common.constants import HTTP_API_ROUTE
from utils import fix_dict_keys
class Route:
def __init__(self, method, route,
data=None, params=None, json=None):
self._method = method
self._route = route
self._data = data
self._params = params
self._json = json
class HTTPClient:
def __init__(self, loop=None):
self._loop = loop
self._host = HTTP_API_ROUTE
async def request(self, route, content_type="json"):
session = ClientSession(loop=self._loop)
kwargs = {}
url = self._host + route._route
if route._data:
kwargs['data'] = route._data
if route._params:
kwargs['params'] = route._params
if route._json:
kwargs['json'] = route._json
r = await session.request(route._method, url, **kwargs)
try:
if r.status is 200:
if content_type == 'json':
data = await r.json()
elif content_type == 'image':
data = await r.read()
else:
data = await r.text()
return data
return None
except Exception as e:
(e)
finally:
await r.release()
await session.close()
##
# Main Data Provider
##
async def is_username_taken(self, character_name):
response = await self.request(
Route("GET", "/character/name/" + character_name))
return response['resp']
async def login(self, username, password):
route = Route(
"POST", "/login",
data={
'username': username,
'password': password,
}
)
response = await self.request(route)
return response
async def get_characters(self, account_id, world_id=None):
if not world_id:
url = f"/account/{account_id}/characters"
else:
url = f"/account/{account_id}/characters/{world_id}"
response = fix_dict_keys(await self.request(Route("GET", url)))
return [Character.from_data(**character) for character in response['characters']]
async def create_new_character(self, account_id, character:Character):
route = Route(
"POST", f"/account/{account_id}/character/new",
json=character.__serialize__()
)
response = await self.request(route)
return response['id']
##
# Static Data Provider
##
async def get_item(self, item_id):
route = Route("GET", f"/item/{item_id}")
response = await self.request(route)
item = ItemSlotEquip(**response)
return item

7
mapy/__init__.py Normal file
View File

@ -0,0 +1,7 @@
__all__ = "log", "WvsCenter", "constants", "Packet", "CSendOps", "CRecvOps"
from .common import constants
from .logger import log
from .server import WvsCenter
from .net.packet import Packet
from .net.opcodes import CSendOps, CRecvOps

View File

@ -4,4 +4,4 @@ from .character_entry import CharacterEntry
from .modifiers import CharacterModifiers
from .character_stats import CharacterStats
from .func_key import FuncKey, FuncKeys
from .skill_entry import SkillEntry
from .skill_entry import SkillEntry

View File

@ -1,16 +1,16 @@
class Account:
def __init__(
self,
id=0,
username="",
password="",
gender=0,
creation="",
last_login="",
last_ip="",
ban=0,
admin=0,
last_connected_world=0
self,
id=0,
username="",
password="",
gender=0,
creation="",
last_login="",
last_ip="",
ban=0,
admin=0,
last_connected_world=0,
):
self.id: int = id
self.username: str = username

View File

@ -1,12 +1,12 @@
from common import abc
from utils import filter_out_to, Random
from mapy.common import abc
from mapy.field import FieldObject
from mapy.game import item as Item
from mapy.utils.tools import Random, filter_out_to
from .modifiers import CharacterModifiers
from .character_stats import CharacterStats
from .func_key import FuncKeys
from .inventory import InventoryManager, InventoryType
from field import FieldObject
from game import item as Item
from .modifiers import CharacterModifiers
class Character(FieldObject):
@ -83,11 +83,11 @@ class Character(FieldObject):
def encode(self, packet):
packet.encode_long(-1 & 0xFFFFFFFF)
packet.encode_byte(0) # combat orders
packet.encode_byte(0) # combat orders
packet.encode_byte(0)
self.stats.encode(packet)
packet.encode_byte(100) # Buddylist capacity
packet.encode_byte(100) # Buddylist capacity
packet.encode_byte(False)
packet.encode_int(self.stats.money)
@ -133,9 +133,19 @@ class Character(FieldObject):
new_index = index + 100 if index < -100 else index
stickers[new_index] = item
inv_equip = {slot: item for slot, item in self.equip_inventory.items.items() if slot >= 0}
dragon_equip = {slot: item for slot, item in self.equip_inventory.items.items() if slot >= -1100 and slot < -1000}
mechanic_equip = {slot: item for slot, item in self.equip_inventory.items.items() if slot >= -1200 and slot < -1100}
inv_equip = {
slot: item for slot, item in self.equip_inventory.items.items() if slot >= 0
}
dragon_equip = {
slot: item
for slot, item in self.equip_inventory.items.items()
if slot >= -1100 and slot < -1000
}
mechanic_equip = {
slot: item
for slot, item in self.equip_inventory.items.items()
if slot >= -1200 and slot < -1100
}
for inv in [eqp_normal, stickers, inv_equip, dragon_equip, mechanic_equip]:
for slot, item in inv.items():
@ -158,7 +168,7 @@ class Character(FieldObject):
skill.encode(packet)
if False:
packet.encode_int(skill.mastery_level) # is skill needed for mastery
packet.encode_int(skill.mastery_level) # is skill needed for mastery
packet.encode_short(0)
@ -225,8 +235,7 @@ class Character(FieldObject):
packet.encode_byte(0xFF)
packet.encode_int(
0 if not equipped.get(-111) else equipped[-111].item_id)
packet.encode_int(0 if not equipped.get(-111) else equipped[-111].item_id)
# for pet_id in self.pet_ids:
for pet_id in range(3):

View File

@ -1,12 +1,13 @@
from common.enum import InventoryType
from mapy.common import InventoryType
from .character_stats import CharacterStats
from .inventory import Inventory
class CharacterEntry:
def __init__(self, **stats):
self._stats = CharacterStats(**stats)
self._equip = Inventory(InventoryType.EQUIP, 96)
@property
def id(self):
return self._stats.id
@ -24,12 +25,12 @@ class CharacterEntry:
self._stats.encode(packet)
self.encode_look(packet)
packet.encode_byte(0) # VAC
packet.encode_byte(0) # VAC
packet.encode_byte(ranking)
if ranking:
packet.skip(16)
def encode_look(self, packet):
packet.encode_byte(self.stats.gender)
packet.encode_byte(self.stats.skin)
@ -51,7 +52,7 @@ class CharacterEntry:
for index, item in equipped.items():
if index > -100 and equipped.get(index - 100):
eqp_normal[index] = item
else:
new_index = index + 100 if index < -100 else index
stickers[new_index] = item
@ -63,9 +64,8 @@ class CharacterEntry:
packet.encode_byte(0xFF)
packet.encode_int(
0 if not equipped.get(-111) else equipped[-111].item_id)
packet.encode_int(0 if not equipped.get(-111) else equipped[-111].item_id)
# for pet_id in self.pet_ids:
for pet_id in range(3):
packet.encode_int(pet_id)
packet.encode_int(pet_id)

View File

@ -1,14 +1,14 @@
class CharacterSkills(dict):
def __init__(self, parent):
self._parent = parent
async def cast(self, skill_id):
skill = self.get(skill_id)
if skill:
await self._parent.modify.stats(
hp = self._parent.stats.hp - 1,
mp = self._parent.stats.mp - 1,
hp=self._parent.stats.hp - 1,
mp=self._parent.stats.mp - 1,
)
# if skill.level_data.buff_time > 0:
@ -18,6 +18,6 @@ class CharacterSkills(dict):
# buff.generate(skill.level_data)
# await self._parent.buff.add(buff)
return True
return False
return False

View File

@ -1,52 +1,54 @@
from dataclasses import dataclass, field
from typing import List
from common import abc
from mapy.common import WildcardData
def default_extend_sp():
return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
def default_pet_locker():
return [0, 0, 0]
@dataclass
class CharacterStats(abc.WildcardData):
id: int = 0
name: str = "Maple Char"
world_id: int = 0
class CharacterStats(WildcardData):
id: int = 0
name: str = "Maple Char"
world_id: int = 0
gender: int = 0
skin: int = 0
face: int = 20001
hair: int = 30003
skin: int = 0
face: int = 20001
hair: int = 30003
level: int = 10
job: int = 0
str: int = 10
dex: int = 10
int: int = 10
luk: int = 10
level: int = 10
job: int = 0
str: int = 10
dex: int = 10
int: int = 10
luk: int = 10
hp: int = 50
m_hp: int = 50
mp: int = 5
m_mp: int = 5
hp: int = 50
m_hp: int = 50
mp: int = 5
m_mp: int = 5
ap: int = 0
sp: int = 0
extend_sp: List = field(default_factory=default_extend_sp)
ap: int = 0
sp: int = 0
extend_sp: List = field(default_factory=default_extend_sp)
exp: int = 0
money: int = 0
fame: int = 0
exp: int = 0
money: int = 0
fame: int = 0
temp_exp: int = 0
field_id: int = 100000000
portal: int = 0
play_time: int = 0
sub_job: int = 0
pet_locker: List = field(default_factory=default_pet_locker)
temp_exp: int = 0
field_id: int = 100000000
portal: int = 0
play_time: int = 0
sub_job: int = 0
pet_locker: List = field(default_factory=default_pet_locker)
def encode(self, packet) -> None:
packet.encode_int(self.id)

View File

@ -1,10 +1,12 @@
from dataclasses import dataclass
@dataclass
class FuncKey:
type: int
action: int
class FuncKeys:
def __init__(self, character):
self._parent = character
@ -12,8 +14,6 @@ class FuncKeys:
def __setitem__(self, key, value):
self._func_keys[key] = value
def __getitem__(self, key):
return self._func_keys.get(key, FuncKey(0, 0))

View File

@ -1,6 +1,5 @@
from common import abc
from common.enum import InventoryType
from game import item as Item
from mapy.common import Inventory as _Inventory, InventoryType
from mapy.game import item as Item
class InventoryManager:
@ -71,8 +70,7 @@ class Tracker:
for _, inv in self._items.items():
for _, item in inv.items():
if (item is None
or item.inventory_item_id in self._starting):
if item is None or item.inventory_item_id in self._starting:
continue
throwaway.append(item.inventory_item_id)
@ -86,7 +84,7 @@ class Tracker:
self._starting.append(item.inventory_item_id)
class Inventory(abc.Inventory):
class Inventory(_Inventory):
def __init__(self, type_, slots):
self._unique_id = None
self.type = type_

View File

@ -1,12 +1,13 @@
from common.enum import StatModifiers
from common.constants import is_extend_sp_job
from utils import CPacket
from mapy.common import StatModifiers
from mapy.common.constants import is_extend_sp_job
from mapy.utils.cpacket import CPacket
class StatModifier:
def __init__(self, character_stats):
self._modifiers = []
self._stats = character_stats
@property
def modifiers(self):
return self._modifiers
@ -16,14 +17,14 @@ class StatModifier:
_flag = 0
for mod in self._modifiers:
_flag |= mod.value
return _flag
return _flag
def alter(self, **stats):
for key, val in stats.items():
modifier = StatModifiers[key.upper()]
self._modifiers.append(modifier)
setattr(self._stats, key, val)
def encode(self, packet):
packet.encode_int(self.flag)
@ -37,13 +38,15 @@ class StatModifier:
else:
packet.encode_short(self._stats.sp)
else:
getattr(packet, f"encode_{modifier.encode}")(packet, getattr(self._stats, modifier.name.lower()))
getattr(packet, f"encode_{modifier.encode}")(
packet, getattr(self._stats, modifier.name.lower())
)
class CharacterModifiers:
def __init__(self, character):
self._parent = character
async def stats(self, *, excl_req=True, **stats):
modifier = StatModifier(self._parent.stats)
modifier.alter(**stats)

View File

@ -0,0 +1,24 @@
from dataclasses import dataclass, field
from typing import List
from mapy.common import WildcardData
from mapy.common.constants import PERMANENT
def default_level_data():
return []
@dataclass
class SkillEntry(WildcardData):
id: int = 0
level: int = 0
mastery_level: int = 0
max_level: int = 0
expiration: int = field(default=PERMANENT)
level_data: List = field(default_factory=default_level_data)
def encode(self, packet):
packet.encode_int(self.id)
packet.encode_int(self.level)
packet.encode_long(PERMANENT) # skill.expiration

View File

@ -1,3 +1,3 @@
from .client_base import ClientBase
from .wvs_game_client import WvsGameClient
from .wvs_login_client import WvsLoginClient
from .wvs_login_client import WvsLoginClient

137
mapy/client/client_base.py Normal file
View File

@ -0,0 +1,137 @@
from mapy.net.packet import Packet
from mapy.net.crypto import (
MapleIV,
decrypt_transform,
encrypt_transform,
MapleAes,
)
from mapy.common.constants import VERSION, SUB_VERSION, LOCALE
from random import randint
from asyncio import get_event_loop, Lock
from mapy import logger as log
class ClientSocket:
def __init__(self, socket):
self._loop = get_event_loop()
self._socket = socket
self._lock = Lock()
self.recieve_size = 16384
self.m_riv = None
self.m_siv = None
self._is_alive = False
self._overflow = None
@property
def identifier(self):
return self._socket.getpeername()
def close(self):
return self._socket.close()
async def receive(self, client):
self._is_alive = True
while self._is_alive:
if not self._overflow:
m_recv_buffer = await self._loop.sock_recv(
self._socket, self.recieve_size
)
if not m_recv_buffer:
await client._parent.on_client_disconnect(client)
return
else:
m_recv_buffer = self._overflow
self._overflow = None
if self.m_riv:
async with self._lock:
length = MapleAes.get_length(m_recv_buffer)
if length != len(m_recv_buffer) - 4:
self._overflow = m_recv_buffer[length + 4 :]
m_recv_buffer = m_recv_buffer[: length + 4]
m_recv_buffer = self.manipulate_buffer(m_recv_buffer)
client.dispatch(Packet(m_recv_buffer))
async def send_packet(self, out_packet):
packet_length = len(out_packet)
packet = bytearray(out_packet.getvalue())
buf = packet[:]
final_length = packet_length + 4
final = bytearray(final_length)
async with self._lock:
MapleAes.get_header(final, self.m_siv, packet_length, VERSION)
buf = encrypt_transform(buf)
final[4:] = MapleAes.transform(buf, self.m_siv)
await self._loop.sock_sendall(self._socket, final)
async def send_packet_raw(self, packet):
await self._loop.sock_sendall(self._socket, packet.getvalue())
def manipulate_buffer(self, buffer):
buf = bytearray(buffer)[4:]
buf = MapleAes.transform(buf, self.m_riv)
buf = decrypt_transform(buf)
return buf
class ClientBase:
def __init__(self, parent, socket):
self.m_socket = socket
self._parent = parent
self.port = None
self._is_alive = False
self.logged_in = False
self.world_id = None
self.channel_id = None
async def initialize(self):
self.m_socket.m_siv = MapleIV(randint(0, 2 ** 31 - 1))
# self.m_socket.m_siv = MapleIV(100)
self.m_socket.m_riv = MapleIV(randint(0, 2 ** 31 - 1))
# self.m_socket.m_riv = MapleIV(50)
packet = Packet(op_code=0x0E)
packet.encode_short(VERSION)
packet.encode_string(SUB_VERSION)
packet.encode_int(self.m_socket.m_riv.value)
packet.encode_int(self.m_socket.m_siv.value)
packet.encode_byte(LOCALE)
await self.send_packet_raw(packet)
await self.m_socket.receive(self)
def dispatch(self, packet):
self._parent.dispatcher.push(self, packet)
async def send_packet(self, packet):
log.packet(f"{packet.name} {self.ip} {packet.to_string()}", "out")
await self.m_socket.send_packet(packet)
async def send_packet_raw(self, packet):
log.packet(f"{packet.name} {self.ip} {packet.to_string()}", "out")
await self.m_socket.send_packet_raw(packet)
@property
def parent(self):
return self._parent
@property
def ip(self):
return self.m_socket.identifier[0]
@property
def data(self):
return self._parent.data

View File

@ -14,4 +14,4 @@ class WvsGameClient(ClientBase):
await self.character.field.broadcast(packet, self)
def get_field(self):
return self._parent.get_field(self.character.field_id)
return self._parent.get_field(self.character.field_id)

View File

@ -22,10 +22,9 @@ class WvsLoginClient(ClientBase):
self.avatars = []
async def login(self, username, password):
resp, account = await (self.data
.account(username=username,
password=password)
.login())
resp, account = await (
self.data.account(username=username, password=password).login()
)
if not resp:
self.account = account
@ -35,10 +34,10 @@ class WvsLoginClient(ClientBase):
return resp
async def load_avatars(self, world_id=None):
self.avatars = await (self.data
.account(id=self.account.id)
.get_entries(world_id=world_id))
self.avatars = await (
self.data.account(id=self.account.id).get_entries(world_id=world_id)
)
@property
def account_id(self):
return getattr(self.account, 'id', -1)
return getattr(self.account, "id", -1)

11
mapy/common/__init__.py Normal file
View File

@ -0,0 +1,11 @@
__all__ = (
"WildcardData",
"InventoryType",
"Inventory",
"StatModifiers",
"WorldFlag",
"Worlds",
)
from .abc import WildcardData, Inventory
from .enum import InventoryType, StatModifiers, WorldFlag, Worlds

View File

@ -1,22 +1,23 @@
from abc import ABCMeta
from copy import deepcopy
from random import randint
class Serializable(metaclass=ABCMeta):
def __serialize__(self):
serialized = {}
for key, value in self.__dict__.items():
if isinstance(value, Serializable):
if issubclass(value.__class__, Serializable):
value = value.__serialize__()
serialized[key] = value
return serialized
class WildcardData:
@classmethod
def __new__(cls, *args, **kwargs):
old_init = cls.__init__
def _new_init_(self, *args, **kwargs):
cleaned = {}
for key, value in kwargs.items():
@ -24,12 +25,13 @@ class WildcardData:
continue
cleaned[key] = value
old_init(self, *args, **cleaned)
cls.__init__ = _new_init_
return super(WildcardData, cls).__new__(cls)
class Inventory:
pass
...
# def add(self, item, slot=None):
# if isinstance(item, ItemSlotEquip):
# free_slot = self.get_free_slot() if not slot else slot
@ -46,4 +48,4 @@ class Inventory:
# # or insert into free slot
# pass
# return items
# return items

View File

@ -45,27 +45,73 @@ DISABLE_CHARACTER_CREATION = False
PERMANENT = 150841440000000000
ANTIREPEAT_BUFFS = [
11101004, 5221000, 11001001, 5211007,
5121000, 5121007, 5111007, 4341000,
5111007, 4121000, 4201003, 2121000,
1221000, 1201006, 1211008, 1211009,
1211010, 1121000, 1001003, 1101006,
1111007, 2101001, 2101003, 1321000,
1311007, 1311006
11101004,
5221000,
11001001,
5211007,
5121000,
5121007,
5111007,
4341000,
5111007,
4121000,
4201003,
2121000,
1221000,
1201006,
1211008,
1211009,
1211010,
1121000,
1001003,
1101006,
1111007,
2101001,
2101003,
1321000,
1311007,
1311006,
]
EVENT_VEHICLE_SKILLS = [
1025, 1027, 1028, 1029, 1030, 1031, 1033,
1034, 1035, 1036, 1037, 1038, 1039, 1040,
1042, 1044, 1049, 1050, 1051, 1052, 1053,
1054, 1063, 1064, 1065, 1069, 1070, 1071
1025,
1027,
1028,
1029,
1030,
1031,
1033,
1034,
1035,
1036,
1037,
1038,
1039,
1040,
1042,
1044,
1049,
1050,
1051,
1052,
1053,
1054,
1063,
1064,
1065,
1069,
1070,
1071,
]
def is_event_vehicle_skill(skill_id):
return skill_id % 10000 in EVENT_VEHICLE_SKILLS
def get_job_from_creation(job_id):
return {0: 3000, 1: 0, 2: 1000, 3: 2000, 4: 2001}.get(job_id, 0)
def is_extend_sp_job(job_id):
return job_id / 1000 == 3 or job_id / 100 == 22 or job_id == 2001
return job_id / 1000 == 3 or job_id / 100 == 22 or job_id == 2001

76
mapy/common/enum.py Normal file
View File

@ -0,0 +1,76 @@
from enum import Enum
class Worlds(Enum):
Scania = 0
Broa = 1
Windia = 2
Khaini = 3
Bellocan = 4
Mardia = 5
Kradia = 6
Yellonde = 7
Galacia = 8
El_Nido = 9
Zenith = 11
Arcenia = 12
Judis = 13
Plana = 14
Kastia = 15
Kalluna = 16
Stius = 17
Croa = 18
Medere = 19
class WorldFlag(Enum):
Null = 0x00
Event = 0x01
New = 0x02
Hot = 0x03
class InventoryType(Enum):
TRACKER = 0x0
EQUIP = 0x1
CONSUME = 0x2
INSTALL = 0x3
ETC = 0x4
CASH = 0x5
class StatModifiers(int, Enum):
def __new__(cls, value, encode_type):
obj = int.__new__(cls, value)
obj._value_ = value
obj.encode = encode_type
return obj
SKIN = (0x1, "byte")
FACE = (0x2, "int")
HAIR = (0x4, "int")
PET = (0x8, "long")
PET2 = (0x80000, "long")
PET3 = (0x100000, "long")
LEVEL = (0x10, "byte")
JOB = (0x20, "short")
STR = (0x40, "short")
DEX = (0x80, "short")
INT = (0x100, "short")
LUK = (0x200, "short")
HP = (0x400, "int")
MAX_HP = (0x800, "int")
MP = (0x1000, "int")
MAX_MP = (0x2000, "int")
AP = (0x4000, "short")
SP = (0x8000, "short")
EXP = (0x10000, "int")
POP = (0x20000, "short")
MONEY = (0x40000, "int")
# TEMP_EXP = 0x200000

1
mapy/db/__init__.py Normal file
View File

@ -0,0 +1 @@
from .db_client import DatabaseClient, Account

View File

@ -1,21 +1,30 @@
import json
from asyncio import get_event_loop
from copy import deepcopy
from datetime import date
from typing import Union
from asyncpg import PostgresError, create_pool
from asyncpg.exceptions import InterfaceError
from character import Account as Account_
from character import Character, CharacterEntry, FuncKey, SkillEntry
from field import Field as field
from field import Foothold, Mob, Npc, Portal
from game import ItemSlotEquip, SkillLevelData
from game import item as Item # ItemSlotBundle,
from utils import get, log
from mapy import log
from mapy.character import Account as Account_
from mapy.character import Character, CharacterEntry, FuncKey, SkillEntry
from mapy.field import Field as field
from mapy.field import Foothold, Mob, Npc, Portal
from mapy.game import ItemSlotEquip, SkillLevelData
from mapy.game import item as Item # ItemSlotBundle,
from mapy.utils import get
from .schema import (Column, Insert, IntColumn, ListArguments, Query, Schema,
StringColumn, Table, Update)
from .schema import (
Column,
Insert,
IntColumn,
ListArguments,
Query,
Schema,
StringColumn,
Table,
Update,
)
from .structure import RMDB, Maplestory
@ -51,13 +60,15 @@ SKILLS = f"{DATABASE}.skills"
class DatabaseClient:
_name = "Database Client"
def __init__(self,
user="postgres",
password="",
host="127.0.0.1",
port=5432,
database="postgres",
loop=None):
def __init__(
self,
user="postgres",
password="",
host="127.0.0.1",
port=5432,
database="postgres",
loop=None,
):
self._user = user
self._pass = password
self._host = host
@ -71,23 +82,24 @@ class DatabaseClient:
@property
def dsn(self):
return ("postgres://"
f"{self._user}:{self._pass}"
f"@{self._host}:{self._port}"
f"/{self._database}")
return (
"postgres://"
f"{self._user}:{self._pass}"
f"@{self._host}:{self._port}"
f"/{self._database}"
)
def log(self, message, level=None):
level = level if level else 'info'
level = level if level else "info"
getattr(log, level)(f"{self._name} {message}")
async def start(self):
# try:
self.pool = await create_pool(
self.dsn,
loop=self._loop,
init=init_conn)
self.log(f"PostgreSQL Client connected at <lc>{self._host}</lc>:"
f"<lr>{self._port}</lr>")
self.pool = await create_pool(self.dsn, loop=self._loop, init=init_conn)
self.log(
f"PostgreSQL Client connected at <lc>{self._host}</lc>:"
f"<lr>{self._port}</lr>"
)
# await Maplestory.create()
@ -334,7 +346,8 @@ class Characters(QueryTable):
)
else:
characters = await (
self.query().where(account_id=self.account_id, world_id=world_id)
self.query()
.where(account_id=self.account_id, world_id=world_id)
.order_by("id")
.get()
)
@ -375,7 +388,9 @@ class Characters(QueryTable):
# Get static data from rmdb for character skills
for skill in skills:
level_data = await self._db.skills.get_skill_level_data(skill["skill_id"])
character.skills[skill["skill_id"]] = SkillEntry(level_data=level_data, **skill)
character.skills[skill["skill_id"]] = SkillEntry(
level_data=level_data, **skill
)
return character
@ -525,8 +540,7 @@ class Inventories:
equips.append(equip_data)
q = (
await self.items_table
.insert.row(items)
await self.items_table.insert.row(items)
.primaries("inventory_item_id")
.returning("inventory_items.inventory_item_id, inventory_items.position")
.commit(do_update=True)

View File

@ -2,12 +2,12 @@ from asyncpg import PostgresError
class SchemaError(PostgresError):
pass
...
class ResponseError(PostgresError):
pass
...
class QueryError(PostgresError):
pass
...

View File

@ -20,10 +20,7 @@ def default_tmpl(c, o, v):
class SQLOperator:
def __init__(self,
sql_oper: str,
py_oper: str,
str_tmpl: str = None):
def __init__(self, sql_oper: str, py_oper: str, str_tmpl: str = None):
self.__sql_oper = sql_oper
self.__py_oper = py_oper
self.__str_tmpl = default_tmpl if not str_tmpl else str_tmpl
@ -82,7 +79,9 @@ class SQLOperator:
@classmethod
def between(cls):
return cls("BETWEEN", None, lambda c, o, mn_v, mx_v: f"{c} {o} {mn_v} AND {mx_v}")
return cls(
"BETWEEN", None, lambda c, o, mn_v, mx_v: f"{c} {o} {mn_v} AND {mx_v}"
)
@classmethod
def in_(cls):
@ -260,10 +259,12 @@ class Column:
return SQLComparison(SQLOperator.in_(), self.aggregate, self.full_name, value)
def contains(self, value): # *values
def contains(self, value): # *values
# if (isinstance(v, Column) == True for v in values):
if isinstance(value, Column) or isinstance(value, (int, str, bool)):
return SQLComparison(SQLOperator.contains(), self.aggregate, self.full_name, value)
return SQLComparison(
SQLOperator.contains(), self.aggregate, self.full_name, value
)
@classmethod
def from_dict(cls, data):
@ -282,7 +283,7 @@ class Column:
elif isinstance(self.default, bool):
default = str(self.default).upper()
elif isinstance(self.data_type, ArraySQL):
default = f"{default}::{self.data_type.to_sql()}"
default = f"{self.default}::{self.data_type.to_sql()}"
else:
default = f"{self.default}"
sql.append(f"DEFAULT {default}")
@ -932,9 +933,7 @@ class Query:
)
elif self._inner_join:
sql.append(
(f"INNER JOIN {self._inner_join} " f"USING({self._using}) ")
)
sql.append((f"INNER JOIN {self._inner_join} " f"USING({self._using}) "))
if self.conditions._queued_conditions:
self.conditions.add_conditions()

619
mapy/db/structure.py Normal file
View File

@ -0,0 +1,619 @@
from enum import Enum
class Meta(Enum):
def __str__(self):
return f"{self.__class__.__name__.lower()}.{super().__str__()}"
class Schema(Meta):
def __new__(cls, value):
value.schema = cls.__name__.lower()
value.primary_key = value.__dict__.get("__primary_key__")
value.foreign_keys = value.__dict__.get("__foreign_keys__")
value.columns = [item.lower() for item in value._member_names_]
return value
def __str__(self):
return f"{self.__class__.__name__.lower()}.{super().__str__()}"
def __getattr__(self, name):
if name not in ("_value_"):
return getattr(self.value, name)
return super().__getattr__(name)
@classmethod
def create(cls):
# async def create(db):
# schema = db.schema(cls.__name__.lower())
# await schema.create(skip_if_exists=False)
# fks = []
# for table in cls:
# t = db.table(table._name_.lower(), schema=schema)
# if await t.exists(): continue
# cols = []
# for c in table:
# primary_key = c.getattr('primary_key', False)
# col = Column(c._name_.lower(), c.data_type, primary_key=primary_key, **c.options)
# if t.foreign_keys and c.name in getattr(table, 'foreign_keys', []):
# fks.append(types.ForeignKey(t, col, sql_type=c.data_type))
# cols.append(col)
# await t.create(*cols)
# for fkey in fks:
# await db.execute_transaction(fk.to_sql())
# return create
pass
class Table(int, Meta):
_ignore_ = "data_type"
def __new__(cls, data_type, options=None):
value = len(cls.__members__) + 1
enum_class = super().__new__(cls, value)
enum_class._value_ = value
enum_class.data_type = data_type
# if data_type in ['integer', 'serial']:
# enum_class.data_type = types.Integer(auto_increment=data_type == 'serial')
# elif data_type == 'smallint':
# enum_class.data_type = types.Integer(small=True)
# elif data_type == 'bigint':
# enum_class.data_type = types.Integer(big=True)
# elif data_type in ['varchar', 'text']:
# enum_class.data_type = types.String()
# elif data_type == 'jsonb':
# enum_class.data_type = types.JSON()
# elif data_type == 'double':
# enum_class.data_type = types.Double()
# elif data_type == 'boolean':
# enum_class.data_type = types.Boolean()
# elif data_type == 'date':
# enum_class.data_type = types.Date()
# from re import search
# if search(r'(\[\])', data_type):
# enum_class.data_type = types.ArraySQL(enum_class.data_type)
enum_class.options = options if options else {}
return enum_class
class ItemConsumeableData(Table):
ITEM_ID = "integer"
FLAGS = "varchar[]"
CURES = "varchar[]"
HP = "smallint"
HP_PERCENTAGE = "smallint"
MAX_HP_PERCENTAGE = "smallint"
MP = "smallint"
MP_PERCENTAGE = "smallint"
MAX_MP_PERCENTAGE = "smallint"
WEAPON_ATTACK = "smallint"
WEAPON_ATTACK_PERCENTAGE = "smallint"
WEAPON_DEFENSE = "smallint"
WEAPON_DEFENSE_PERCENTAGE = "smallint"
MAGIC_ATTACK = "smallint"
MAGIC_ATTACK_PERCENTAGE = "smallint"
MAGIC_DEFENSE = "smallint"
MAGIC_DEFENSE_PERCENTAGE = "smallint"
ACCURACY = "smallint"
ACCURACY_PERCENTAGE = "smallint"
AVOID = "smallint"
AVOID_PERCENTAGE = "smallint"
SPEED = "smallint"
SPEED_PERCENTAGE = "smallint"
JUMP = "smallint"
SECONDARY_STAT = "jsonb"
BUFF_TIME = "integer"
PROB = "smallint"
EVENT_POINT = "integer"
MOB_ID = "integer"
MOB_HP = "integer"
SCREEN_MESSAGE = "text"
ATTACK_INDEX = "smallint"
ATTACK_MOB_ID = "integer"
MOVE_TO = "integer"
RETURN_MAP_QR = "integer"
DECREASE_HUNGER = "smallint"
MORPH = "smallint"
CARNIVAL_TYPE = "smallint"
CARNIVAL_POINTS = "smallint"
CARNIVAL_SKILL = "smallint"
EXPERIENCE = "integer"
__primary_key__ = ("ITEM_ID",)
class ItemData(Table):
ITEM_ID = "integer"
INVENTORY = "smallint"
PRICE = ("integer", {"default": 0})
MAX_SLOT_QUANTITY = "smallint"
MAX_POSSESSION_COUNT = "smallint"
MIN_LEVEL = "smallint"
MAX_LEVEL = ("smallint", {"default": 250})
EXPERIENCE = "integer"
MONEY = "integer"
STATE_CHANGE_ITEM = "integer"
LEVEL_FOR_MAKER = "smallint"
NPC = "integer"
FLAGS = ("varchar[]", {"default": "'{}'"})
PET_LIFE_EXTEND = "smallint"
MAPLE_POINT = "integer"
MONEY_MIN = "integer"
MONEY_MAX = "integer"
EXP_RATE = "double"
ADD_TIME = "smallint"
SLOT_INDEX = "smallint"
__primary_key__ = ("ITEM_ID",)
class ItemEquipData(Table):
ITEM_ID = "integer"
FLAGS = ("varchar[]", {"default": "'{}'"})
EQUIP_SLOTS = ("varchar[]", {"default": "'{}'"})
ATTACK_SPEED = ("smallint", {"default": 0})
RUC = ("smallint", {"default": 0})
REQ_STR = ("smallint", {"default": 0})
REQ_DEX = ("smallint", {"default": 0})
REQ_INT = ("smallint", {"default": 0})
REQ_LUK = ("smallint", {"default": 0})
REQ_FAME = ("smallint", {"default": 0})
REQ_JOB = ("smallint[]", {"default": "'{}'"})
HP = ("smallint", {"default": 0})
HP_PERCENTAGE = ("smallint", {"default": 0})
MP = ("smallint", {"default": 0})
MP_PERCENTAGE = ("smallint", {"default": 0})
STR = ("smallint", {"default": 0})
DEX = ("smallint", {"default": 0})
INT = ("smallint", {"default": 0})
LUK = ("smallint", {"default": 0})
HANDS = ("smallint", {"default": 0})
WEAPON_ATTACK = ("smallint", {"default": 0})
WEAPON_DEFENSE = ("smallint", {"default": 0})
MAGIC_ATTACK = ("smallint", {"default": 0})
MAGIC_DEFENSE = ("smallint", {"default": 0})
ACCURACY = ("smallint", {"default": 0})
AVOID = ("smallint", {"default": 0})
JUMP = ("smallint", {"default": 0})
SPEED = ("smallint", {"default": 0})
TRACTION = ("double", {"default": 0})
RECOVERY = ("double", {"default": 0})
KNOCKBACK = ("smallint", {"default": 0})
TAMING_MOB = ("smallint", {"default": 0})
DURABILITY = ("integer", {"default": "'-1'::integer"})
INC_LIGHTNING_DAMAGE = ("smallint", {"default": 0})
INC_ICE_DAMAGE = ("smallint", {"default": 0})
INC_FIRE_DAMAGE = ("smallint", {"default": 0})
INC_POISON_DAMAGE = ("smallint", {"default": 0})
ELEMENTAL_DEFAULT = ("smallint", {"default": 0})
CRAFT = ("smallint", {"default": 0})
SET_ID = ("integer", {"default": 0})
ENCHANT_CATEGORY = ("smallint", {"default": 0})
HEAL_HP = ("smallint", {"default": 0})
SPECIAL_ID = ("integer", {"default": 0})
__primary_key__ = "ITEM_ID"
class ItemPetData(Table):
ITEM_ID = "integer"
HUNGER = "smallint"
LIFE = "smallint"
LIMITED_LIFE = "integer"
EVOLUTION_ITEM = "integer"
EVOLUTION_LEVEL = "smallint"
FLAGS = "varchar[]"
__primary_key__ = ("ITEM_ID",)
class ItemRechargeableData(Table):
ITEM_ID = "integer"
UNIT_PRICE = "smallint"
WEAPON_ATTACK = "smallint"
__primary_key__ = "ITEM_ID"
class MapData(Table):
MAP_ID = "integer"
MAP_NAME = ("text", {"default": "''::text"})
STREET_NAME = ("text", {"default": "''::text"})
MAP_MARK = ("text", {"default": "''::text"})
FLAGS = ("varchar[]", {"default": "'{}'"})
MOB_RATE = ("double", {"default": 1.0})
FIXED_MOB_CAPACITY = "smallint"
SPAWN_MOB_INTERVAL = "smallint"
DROP_RATE = ("double", {"default": 1.0})
REGEN_RATE = ("double", {"default": 1.0})
SHUFFLE_NAME = ("text", {"default": "NULL::varchar"})
DEFAULT_BGM = ("text", {"default": "NULL::varchar"})
EFFECT = ("text", {"default": "NULL::varchar"})
MIN_LEVEL_LIMIT = "smallint"
MAX_LEVEL_LIMIT = "smallint"
TIME_LIMIT = "smallint"
DEFAULT_TRACTION = ("double", {"default": 1})
MAP_LTX = "smallint"
MAP_LTY = "smallint"
MAP_RBX = "smallint"
MAP_RBY = "smallint"
LP_BOTTOM = "smallint"
LP_TOP = "smallint"
LP_SIDE = "smallint"
FORCED_MAP_RETURN = "integer"
FIELD_TYPE = "smallint"
DECREASE_HP = "integer"
DECREASE_MP = "integer"
DECREASE_INTERVAL = "smallint"
DAMAGE_PER_SECOND = "smallint"
PROTECT_ITEM = "integer"
SHIP_KIND = "smallint"
CONSUME_COOLDOWN = "smallint"
LINK = "integer"
FIELD_LIMITATIONS = ("bigint", {"default": 0})
RETURN_MAP = "integer"
MAP_LIMIT = "smallint"
MAP_VERSION = "smallint"
ON_FIRST_USER_ENTER = "text"
ON_USER_ENTER = "text"
DESCRIPTION = ("text", {"default": "''::text"})
MOVE_LIMIT = "smallint"
PROTECT_SET = "integer"
ALLOWED_ITEM_DROP = "integer"
DROP_EXPIRE = "smallint"
TIME_OUT_LIMIT = "smallint"
PHASE_ALPHA = "smallint"
PHASE_BACKGROUND = "smallint"
TIME_MOB_SPAWN = "smallint"
__primary_key__ = "MAP_ID"
class MapFootholds(Table):
MAP_ID = "integer"
GROUP_ID = "smallint"
ID = "smallint"
X_1 = "smallint"
Y_1 = "smallint"
X_2 = "smallint"
Y_2 = "smallint"
DRAG_FORCE = "smallint"
PREVIOUS_ID = "smallint"
NEXT_ID = "smallint"
FLAGS = "varchar[]"
__primary_key__ = ("MAP_ID", "GROUP_ID", "ID")
class MapLife(Table):
MAP_ID = "integer"
LIFE_ID = "integer"
LIFE_TYPE = "text"
LIFE_NAME = ("text", {"default": "''::text"})
X_POS = "smallint"
Y_POS = "smallint"
FOOTHOLD = "smallint"
MIN_CLICK_POS = "smallint"
MAX_CLICK_POS = "smallint"
RESPAWN_TIME = "integer"
FLAGS = ("varchar[]", {"default": "'{}'"})
CY = "smallint"
FACE = ("boolean", {"default": 0})
HIDE = ("boolean", {"default": 0})
__primary_key__ = None
class MapPortals(Table):
MAP_ID = "integer"
ID = "smallint"
NAME = "text"
X_POS = "smallint"
Y_POS = "smallint"
DESTINATION = "integer"
DESTINATION_LABEL = "text"
PORTAL_TYPE = "smallint"
__primary_key__ = ("MAP_ID", "ID")
class MapReactors(Table):
MAP_ID = "integer"
REACTOR_ID = "integer"
REACTOR_TIME = "integer"
NAME = "text"
X = "smallint"
Y = "smallint"
F = "smallint"
__primary_key__ = ("MAP_ID", "REACTOR_ID")
class MapTimedMobs(Table):
MAP_ID = "integer"
MOB_ID = "integer"
START_HOUR = "smallint"
END_HOUR = "smallint"
MESSAGE = "text"
__primary_key__ = ("MAP_ID", "MOB_ID")
class MobData(Table):
MOB_ID = "integer"
MOB_LEVEL = "smallint"
FLAGS = ("varchar[]", {"default": "'{}'"})
HP = "integer"
HP_RECOVERY = "integer"
MP = "integer"
MP_RECOVERY = "integer"
EXPERIENCE = "integer"
KNOCKBACK = "integer"
FIXED_DAMAGE = "integer"
EXPLODE_HP = "integer"
LINK = "integer"
SUMMON_TYPE = "smallint"
DEATH_BUFF = "integer"
DEATH_AFTER = "integer"
TRACTION = ("double", {"default": 1.0})
DAMAGED_BY_SKILL_ONLY = "smallint"
DAMAGED_BY_MOB_ONLY = "integer"
DROP_ITEM_PERIOD = "smallint"
HP_BAR_COLOR = "smallint"
HP_BAR_BG_COLOR = "smallint"
CARNIVAL_POINTS = "smallint"
PHYSICAL_ATTACK = "smallint"
PHYSICAL_DEFENSE = "smallint"
PHYSICAL_DEFENSE_RATE = "smallint"
MAGICAL_ATTACK = "smallint"
MAGICAL_DEFENSE = "smallint"
MAGICAL_DEFENSE_RATE = "smallint"
ACCURACY = "smallint"
AVOID = "smallint"
SPEED = "smallint"
FLY_SPEED = "smallint"
CHASE_SPEED = "smallint"
ICE_MODIFIER = "smallint"
FIRE_MODIFIER = "smallint"
POISON_MODIFIER = "smallint"
LIGHTNING_MODIFIER = "smallint"
HOLY_MODIFIER = "smallint"
DARK_MODIFIER = "smallint"
NONELEMENTAL_MODIFIER = "smallint"
__primary_key__ = "MOB_ID"
class MobSkills(Table):
MOB_ID = "integer"
SKILL_ID = "smallint"
LEVEL = "smallint"
EFFECT_AFTER = "smallint"
__primary_key__ = ("MOB_ID", "SKILL_ID")
class PlayerSkillData(Table):
SKILL_ID = "integer"
FLAGS = "varchar[]"
WEAPON = "smallint"
SUB_WEAPON = "smallint"
MAX_LEVEL = "smallint"
BASE_MAX_LEVEL = "smallint"
SKILL_TYPE = "varchar[]"
ELEMENT = "text"
MOB_COUNT = "text"
HIT_COUNT = "text"
BUFF_TIME = "text"
HP_COST = "text"
MP_COST = "text"
DAMAGE = "text"
FIXED_DAMAGE = "text"
CRITICAL_DAMAGE = "text"
MASTERY = "text"
OPTIONAL_ITEM_COST = "smallint"
ITEM_COST = "integer"
ITEM_COUNT = "smallint"
BULLET_COST = "smallint"
MONEY_COST = "text"
X_PROPERTY = "text"
Y_PROPERTY = "text"
SPEED = "text"
JUMP = "text"
STR = "text"
WEAPON_ATTACK = "text"
WEAPON_DEFENSE = "text"
MAGIC_ATTACK = "text"
MAGIC_DEFENSE = "text"
ACCURACY = "text"
AVOID = "text"
HP = "text"
MP = "text"
PROBABILITY = "text"
LTX = "smallint"
LTY = "smallint"
RBX = "smallint"
RBY = "smallint"
COOLDOWN_TIME = "text"
AVOID_CHANCE = "text"
RANGE = "text"
MORPH = "smallint"
__primary_key__ = "SKILL_ID"
class PlayerSkillRequirementData(Table):
SKILL_ID = "integer"
REQ_SKILL_ID = "integer"
REQ_LEVEL = "smallint"
__primary_key__ = ("SKILL_ID", "REQ_SKILL_ID")
class String(Table):
OBJECT_ID = "integer"
OBJECT_TYPE = "varchar"
DESCRIPTION = "text"
LABEL = "text"
__primary_key__ = ("OBJECT_ID", "OBJECT_TYPE")
class RMDB(Schema):
ITEM_CONSUMEABLE_DATA = ItemConsumeableData
ITEM_DATA = ItemData
ITEM_EQUIP_DATA = ItemEquipData
ITEM_PET_DATA = ItemPetData
ITEM_RECHARGEABLE_DATA = ItemRechargeableData
MAP_DATA = MapData
MAP_FOOTHOLDS = MapFootholds
MAP_LIFE = MapLife
MAP_PORTALS = MapPortals
MAP_REACTORS = MapReactors
MAP_TIMED_MOBS = MapTimedMobs
MOB_DATA = MobData
MOB_SKILLS = MobSkills
PLAYER_SKILL_DATA = PlayerSkillData
PLAYER_SKILL_REQS_DATA = PlayerSkillRequirementData
STRING = String
class Account(Table):
ID = "serial"
USERNAME = "text"
PASSWORD = "text"
SALT = "text"
CREATION = ("date", {"default": "CURRENT_DATE"})
LAST_LOGIN = ("date", {"default": '"1970-01-01"::date'})
LAST_IP = ("inet", {"default": '"0.0.0.0"::inet'})
BAN = ("bool", {"default": 0})
ADMIN = ("bool", {"default": 0})
GENDER = "bool"
__primary_key__ = ("ID",)
# __unqiue_index__ = ("USERNAME")
class Buddies(Table):
pass
class Character(Table):
ID = "integer"
ACCOUNT_ID = "integer"
NAME = "text"
GENDER = "bool"
SKIN = "smallint"
FACE = "smallint"
HAIR = "smallint"
PET_LOCKER = "int[]"
LEVEL = "smallint"
JOB = "smallint"
STR = "smallint"
DEX = "smallint"
INT = "smallint"
LUK = "smallint"
HP = "smallint"
MAX_HP = "smallint"
MP = "smallint"
MAX_MP = "smallint"
AP = "smallint"
SP = "smallint"
EXP = "integer"
MONEY = "integer"
TEMP_EXP = "integer"
FIELD_ID = "integer"
PORTAL = "text"
PLAY_TIME = "integer"
SUB_JOB = "smallint"
FAME = "smallint"
EXTEND_SP = "smallint[]"
WORLD_ID = "smallint"
__primary_key__ = ("ID",)
__foreign_keys__ = {"account_id": "accounts.id"}
class InventoryEquipment(Table):
INVENTORY_ITEM_ID = "bigint"
UPGRADE_SLOTS = "integer"
STR = "smallint"
DEX = "smallint"
INT = "smallint"
LUK = "smallint"
HP = "integer"
MP = "integer"
WEAPON_ATTACK = "smallint"
MAGIC_ATTACK = "smallint"
WEAPON_DEFENSE = "smallint"
MAGIC_DEFENSE = "smallint"
ACCURACY = "smallint"
AVOID = "smallint"
HANDS = "smallint"
SPEED = "smallint"
JUMP = "smallint"
RING_ID = "integer"
CRAFT = "smallint"
ATTRIBUTE = "smallint"
LEVEL_UP_TYPE = "smallint"
LEVEL = "smallint"
IUC = "integer"
GRADE = "smallint"
EXP = "smallint"
CHUC = "smallint"
OPTION_1 = "smallint"
OPTION_2 = "smallint"
OPTION_3 = "smallint"
SOCKET_1 = "smallint"
SOCKET_2 = "smallint"
LISN = "bigint"
CUC = "smallint"
RUC = "smallint"
__primary_key__ = "inventory_item_id"
__foreign_keys__ = {"INVENTORY_ITEM_ID": "inventory_items.inventory_item_id"}
class InventoryItems(Table):
INVENTORY_ITEM_ID = "bigint"
CHARACTER_ID = "integer"
STORAGE_ID = "integer"
ITEM_ID = "integer"
INVENTORY_TYPE = "smallint"
POSITION = "integer"
OWNER = "text"
PET_ID = "bigint"
QUANTITY = "integer"
CISN = "bigint" # Current inventory Serial
SN = "bigint" # Serial Number (Would replace inventory_item_id?)
EXPIRE = "bigint"
__primary_key__ = "inventory_item_id"
# class InventoryPets(Table):
# pass
# class Keymap(Table):
# pass
# class Skills(Table):
# pass
class Maplestory(Schema):
ACCOUNTS = Account
BUDDIES = Buddies
CHARACTERS = Character
INVENTORY_EQUIPMENT = InventoryEquipment
INVENTORY_ITEMS = InventoryItems
# INVENTORY_PETS = InventoryPets
# KEYMAP = Keymap
# SKILLS = Skills

View File

@ -12,13 +12,13 @@ class SQLType:
def to_dict(self):
dct = self.__dict__.copy()
clas = self.__class__
dct['__meta__'] = clas.__module__ + '.' + clas.__qualname__
dct["__meta__"] = clas.__module__ + "." + clas.__qualname__
return dct
@classmethod
def from_dict(cls, data):
meta = data.pop('__meta__')
given = cls.__module__ + '.' + cls.__qualname__
meta = data.pop("__meta__")
given = cls.__module__ + "." + cls.__qualname__
if given != meta:
cls = pydoc.locate(meta)
@ -30,8 +30,7 @@ class SQLType:
return self
def __eq__(self, other):
return isinstance(other, self.__class__)\
and self.__dict__ == other.__dict__
return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
def __ne__(self, other):
return not self.__eq__(other)
@ -47,14 +46,14 @@ class Boolean(SQLType):
python = bool
def to_sql(self):
return 'BOOLEAN'
return "BOOLEAN"
class Date(SQLType):
python = datetime.date
def to_sql(self):
return 'DATE'
return "DATE"
class Datetime(SQLType):
@ -65,16 +64,16 @@ class Datetime(SQLType):
def to_sql(self):
if self.timezone:
return 'TIMESTAMP WITH TIMEZONE'
return "TIMESTAMP WITH TIMEZONE"
return 'TIMESTAMP'
return "TIMESTAMP"
class Double(SQLType):
python = float
def to_sql(self):
return 'REAL'
return "REAL"
class Integer(SQLType):
@ -86,21 +85,21 @@ class Integer(SQLType):
self.auto_increment = auto_increment
if big and small:
raise SchemaError('Integer cannot be both big and small')
raise SchemaError("Integer cannot be both big and small")
def to_sql(self):
if self.auto_increment:
if self.big:
return 'BIGSERIAL'
return "BIGSERIAL"
if self.small:
return 'SMALLSERIAL'
return 'SERIAL'
return "SMALLSERIAL"
return "SERIAL"
if self.big:
return 'BIGINT'
return "BIGINT"
if self.small:
return 'SMALLINT'
return 'INTEGER'
return "SMALLINT"
return "INTEGER"
def is_real_type(self):
return not self.auto_increment
@ -108,32 +107,34 @@ class Integer(SQLType):
class Interval(SQLType):
python = datetime.timedelta
valid_fields = ('YEAR',
'MONTH',
'DAY',
'HOUR',
'MINUTE',
'SECOND',
'YEAR TO MONTH',
'DAY TO HOUR',
'DAY TO MINUTE',
'DAY TO SECOND',
'HOUR TO MINUTE',
'HOUR TO SECOND',
'MINUTE TO SECOND')
valid_fields = (
"YEAR",
"MONTH",
"DAY",
"HOUR",
"MINUTE",
"SECOND",
"YEAR TO MONTH",
"DAY TO HOUR",
"DAY TO MINUTE",
"DAY TO SECOND",
"HOUR TO MINUTE",
"HOUR TO SECOND",
"MINUTE TO SECOND",
)
def __init__(self, field=None):
if field:
field = field.upper()
if field not in self.valid_fields:
raise SchemaError('invalid interval specified')
raise SchemaError("invalid interval specified")
self.field = field
def to_sql(self):
if self.field:
return 'INTERVAL ' + self.field
return 'INTERVAL'
return "INTERVAL " + self.field
return "INTERVAL"
class Decimal(SQLType):
@ -142,8 +143,7 @@ class Decimal(SQLType):
def __init__(self, *, precision=None, scale=None):
if precision is not None:
if precision < 0 or precision > 1000:
raise SchemaError(
'precision must be greater than 0 and below 1000')
raise SchemaError("precision must be greater than 0 and below 1000")
if scale is None:
scale = 0
@ -153,7 +153,7 @@ class Decimal(SQLType):
def to_sql(self):
if self.precision is not None:
return f"NUMERIC({self.precision}, {self.scale})"
return 'NUMERIC'
return "NUMERIC"
class Numeric(SQLType):
@ -162,8 +162,7 @@ class Numeric(SQLType):
def __init__(self, *, precision=None, scale=None):
if precision is not None:
if precision < 0 or precision > 1000:
raise SchemaError("precision must be greater than 0"
"and below 1000")
raise SchemaError("precision must be greater than 0" "and below 1000")
if scale is None:
scale = 0
@ -173,7 +172,7 @@ class Numeric(SQLType):
def to_sql(self):
if self.precision is not None:
return f"NUMERIC({self.precision}, {self.scale})"
return 'NUMERIC'
return "NUMERIC"
class String(SQLType):
@ -184,11 +183,11 @@ class String(SQLType):
self.fixed = fixed
if fixed and length is None:
raise SchemaError('Cannot have fixed string with no length')
raise SchemaError("Cannot have fixed string with no length")
def to_sql(self):
if self.length is None:
return 'TEXT'
return "TEXT"
if self.fixed:
return f"CHAR({self.length})"
return f"VARCHAR({self.length})"
@ -202,39 +201,46 @@ class Time(SQLType):
def to_sql(self):
if self.timezone:
return 'TIME WITH TIME ZONE'
return 'TIME'
return "TIME WITH TIME ZONE"
return "TIME"
class JSON(SQLType):
python = None
def to_sql(self):
return 'JSONB'
return "JSONB"
class ForeignKey(SQLType):
def __init__(self, table, column, *, sql_type=None, on_delete='CASCADE', on_update='NO ACTION'):
def __init__(
self,
table,
column,
*,
sql_type=None,
on_delete="CASCADE",
on_update="NO ACTION",
):
if not table or not isinstance(table, str):
raise SchemaError('Missing table to reference (must be string)')
raise SchemaError("Missing table to reference (must be string)")
valid_actions = (
'NO ACTION',
'RESTRICT',
'CASCADE',
'SET NULL',
'SET DEFAULT',
"NO ACTION",
"RESTRICT",
"CASCADE",
"SET NULL",
"SET DEFAULT",
)
on_delete = on_delete.upper()
on_update = on_update.upper()
if on_delete not in valid_actions:
raise TypeError('on_delete must be one of %s.' % valid_actions)
raise TypeError("on_delete must be one of %s." % valid_actions)
if on_update not in valid_actions:
raise TypeError('on_update must be one of %s.' % valid_actions)
raise TypeError("on_update must be one of %s." % valid_actions)
self.table = table
self.column = column
@ -248,7 +254,7 @@ class ForeignKey(SQLType):
sql_type = sql_type()
if not isinstance(sql_type, SQLType):
raise TypeError('Cannot have non-SQLType derived sql_type')
raise TypeError("Cannot have non-SQLType derived sql_type")
if not sql_type.is_real_type():
raise SchemaError('sql_type must be a "real" type')
@ -259,14 +265,16 @@ class ForeignKey(SQLType):
return False
def to_sql(self):
return (f'{self.column} REFERENCES {self.table} ({self.column})'
f" ON DELETE {self.on_delete} ON UPDATE {self.on_update}")
return (
f"{self.column} REFERENCES {self.table} ({self.column})"
f" ON DELETE {self.on_delete} ON UPDATE {self.on_update}"
)
class ArraySQL(SQLType):
def __init__(self, inner_type, size: int = None):
if not isinstance(inner_type, SQLType):
raise SchemaError('Array inner type must be an SQLType')
raise SchemaError("Array inner type must be an SQLType")
self.type = inner_type
self.size = size

15
mapy/field/__init__.py Normal file
View File

@ -0,0 +1,15 @@
__all__ = (
"MobPool",
"NpcPool",
"UserPool",
"Field",
"FieldObject",
"Foothold",
"Mob",
"Npc",
"Portal",
)
from .field import Field
from .field_object import FieldObject, Foothold, Mob, Npc, Portal
from .pool import MobPool, NpcPool, UserPool

View File

@ -1,10 +1,9 @@
from .foothold_manager import FootholdManager
from .portal_manager import PortalManager
from .mob_pool import MobPool
from .npc_pool import NpcPool
from .user_pool import UserPool
from .pool import MobPool, NpcPool, UserPool
from mapy.utils.cpacket import CPacket
from utils import CPacket, get
class Field:
def __init__(self, map_id):
@ -26,29 +25,33 @@ class Field:
character = client.character
if client.sent_char_data:
await client.send_packet(CPacket.set_field(character, False, client.channel_id))
await client.send_packet(
CPacket.set_field(character, False, client.channel_id)
)
else:
client.sent_char_data = True
character.stats.portal = 0
await client.send_packet(CPacket.set_field(character, True, client.channel_id))
await client.send_packet(
CPacket.set_field(character, True, client.channel_id)
)
for _character in self.clients.characters:
await client.send_packet(CPacket.user_enter_field(_character))
await self.spawn_mobs(client)
await self.spawn_npcs(client)
self.clients.add(client)
await self.broadcast(CPacket.user_enter_field(character))
async def broadcast(self, packet, *ignore):
for client in self.clients:
if client in ignore or client.character in ignore:
continue
await client.send_packet(packet)
async def swap_mob_controller(self, client, mob):
@ -56,7 +59,7 @@ class Field:
controller = next(filter(lambda c: c.character.id == mob.controller), None)
if controller:
await controller.send_packet(CPacket.mob_change_controller(mob, 0))
mob.controller = client.character.id
await client.send_packet(CPacket.mob_change_controller(mob, 1))
@ -64,10 +67,9 @@ class Field:
for mob in self.mobs:
if mob.controller == 0:
await self.swap_mob_controller(client, mob)
await client.send_packet(CPacket.mob_enter_field(mob))
async def spawn_npcs(self, client):
for npc in self.npcs:
await client.send_packet(CPacket.npc_enter_field(npc))

140
mapy/field/field_object.py Normal file
View File

@ -0,0 +1,140 @@
from dataclasses import dataclass
from mapy.common import WildcardData
from mapy.utils import TagPoint
from .move_path import MovePath
class FieldObject(WildcardData):
def __init__(self):
self._obj_id = -1
self._position = MovePath()
self._field = None
@property
def obj_id(self):
return self._obj_id
@obj_id.setter
def obj_id(self, value):
self._obj_id = value
@property
def position(self):
return self._position
@property
def field(self):
return self._field
@field.setter
def field(self, value):
self._field = value
@dataclass
class Foothold(WildcardData):
id: int = 0
prev: int = 0
next: int = 0
x1: int = 0
y1: int = 0
x2: int = 0
y2: int = 0
@property
def wall(self):
return self.x1 == self.x2
def compare_to(self, foothold):
if self.y2 < foothold.y1:
return -1
if self.y1 > foothold.y2:
return 1
return 0
@dataclass
class Portal(WildcardData):
id: int = 0
name: str = ""
type: int = 0
destination: int = 0
destination_label: str = ""
x: int = 0
y: int = 0
def __post_init__(self):
self.point = TagPoint(self.x, self.y)
def __str__(self):
return f"{id} @ {self.point} -> {self.destination}"
@dataclass
class Life(FieldObject):
life_id: int = 0
life_type: str = ""
foothold: int = 0
x: int = 0
y: int = 0
cy: int = 0
f: int = 0
hide: int = 0
rx0: int = 0 # min click position
rx1: int = 0 # max click position
mob_time: int = 0
@dataclass
class Mob(Life):
mob_id: int = 0
hp: int = 0
mp: int = 0
hp_recovery: int = 0
mp_recovery: int = 0
exp: int = 0
physical_attack: int = 0
def __post_init__(self):
self.attackers = {}
self.pos = MovePath(self.x, self.cy, self.foothold)
self.cur_hp = self.hp
self.cur_mp = self.mp
self.controller = 0
@property
def dead(self):
return self.cur_hp <= 0
def damage(self, character, amount):
pass
def encode_init(self, packet):
packet.encode_int(self.obj_id)
packet.encode_byte(5)
packet.encode_int(self.life_id)
# Set Temporary Stat
packet.encode_long(0)
packet.encode_long(0)
packet.encode_short(self.pos.x)
packet.encode_short(self.pos.y)
packet.encode_byte(0 & 1 | 2 * 2)
packet.encode_short(self.pos.foothold)
packet.encode_short(self.pos.foothold)
packet.encode_byte(abs(-2))
packet.encode_byte(0)
packet.encode_int(0)
packet.encode_int(0)
@dataclass
class Npc(Life):
def __post_init__(self):
self.pos = MovePath(self.x, self.cy, self.foothold)
self.id = self.life_id

View File

@ -1,4 +1,5 @@
from net.packets import packet
from mapy.net.packet import ByteBuffer
class MovePath:
def __init__(self, x=0, y=0, foothold=0, position=0):
@ -10,13 +11,13 @@ class MovePath:
self.vy = None
def decode_move_path(self, move_path):
ipacket = packet.ByteBuffer(move_path)
ipacket = ByteBuffer(move_path)
self.x = ipacket.decode_short()
self.y = ipacket.decode_short()
self.vx = ipacket.decode_short()
self.vy = ipacket.decode_short()
size = ipacket.decode_byte()
for i in range(size):
@ -42,6 +43,6 @@ class MovePath:
unk = ipacket.decode_short()
else:
break
def __str__(self):
return f"Position: {self.x},{self.y} - Foothold: {self.foothold} - Stance: {self.stance}"
return f"Position: {self.x},{self.y} - Foothold: {self.foothold} - Stance: {self.stance}"

77
mapy/field/pool.py Normal file
View File

@ -0,0 +1,77 @@
class ObjectPool:
def __init__(self, field):
self.field = field
self.cache = {}
self.uid_base = 1000
@property
def new_uid(self):
self.uid_base += 1
return self.uid_base
def add(self, value):
value.obj_id = self.new_uid
self.cache[value.obj_id] = value
def remove(self, key):
return self.cache.pop(key)
def clear(self):
self.cache = {}
def get(self, key):
return self.cache.get(key, None)
def __enumerator__(self):
return (obj for obj in self.cache.values())
def __iter__(self):
return (obj for obj in self.cache.values())
def __aiter__(self):
return self.__iter__()
class MobPool(ObjectPool):
def __init__(self, field):
super().__init__(field)
self.spawns = []
def add(self, mob):
mob.field = self.field
super().add(mob)
self.spawns.append(mob)
async def remove(self, key):
mob = self.get(key)
if mob:
owner = None
# await self.field.broadcast(CPacket.mob_leave_field(mob))
if mob.dead:
pass
# drop_pos_x = mob.position.x
# drop_pos_y = mob.position.y
# self.field.drops.add(Drop(0, mob.position, 50, 1))
return super().remove(key)
class NpcPool(ObjectPool):
...
class UserPool(ObjectPool):
def add(self, client):
super().add(client)
client.character.field = self.field
@property
def characters(self):
return [client.character for client in self]
def __aiter__(self):
return [client for client in self]

View File

@ -1,2 +1,2 @@
from .skill import Skill, SkillLevel, SkillLevelData
from .item import ItemSlotEquip, ItemSlotBundle
from .item import ItemSlotEquip, ItemSlotBundle

View File

@ -1,17 +1,18 @@
from dataclasses import dataclass, field
from dataclasses import dataclass
from enum import Enum
from typing import List
from common import abc
from mapy.common import WildcardData
class ItemInventoryTypes(Enum):
ItemSlotEquip = 0x1
@dataclass
class ItemSlotBase(abc.WildcardData):
class ItemSlotBase(WildcardData):
"""Base item class for all items
Parameters
----------
item_id: int
@ -29,16 +30,16 @@ class ItemSlotBase(abc.WildcardData):
"""
item_id: int = 0
cisn: int = 0
expire: int = 0
inventory_item_id: int = 0
quantity: int = 0
flag: int = 0
item_id: int = 0
cisn: int = 0
expire: int = 0
inventory_item_id: int = 0
quantity: int = 0
flag: int = 0
def encode(self, packet) -> None:
"""Encode base item information onto packet
Parameters
----------
packet: :class:`net.packets.Packet`
@ -47,58 +48,59 @@ class ItemSlotBase(abc.WildcardData):
"""
packet.encode_int(self.item_id)
packet.encode_byte(self.cisn is not 0)
packet.encode_byte(self.cisn == 0)
if self.cisn:
packet.encode_long(self.cisn)
packet.encode_long(0)
@dataclass
class ItemSlotEquip(ItemSlotBase):
req_job: List = list
ruc: int = 0
cuc: int = 0
req_job: List = list
ruc: int = 0
cuc: int = 0
str: int = 0
dex: int = 0
int: int = 0
luk: int = 0
hp: int = 0
mp: int = 0
weapon_attack: int = 0
str: int = 0
dex: int = 0
int: int = 0
luk: int = 0
hp: int = 0
mp: int = 0
weapon_attack: int = 0
weapon_defense: int = 0
magic_attack: int = 0
magic_defense: int = 0
accuracy: int = 0
avoid: int = 0
magic_attack: int = 0
magic_defense: int = 0
accuracy: int = 0
avoid: int = 0
hands: int = 0
speed: int = 0
jump: int = 0
hands: int = 0
speed: int = 0
jump: int = 0
title: str = ""
craft: int = 0
attribute: int = 0
level_up_type: int = 0
level: int = 0
durability: int = 0
iuc: int = 0
exp: int = 0
title: str = ""
craft: int = 0
attribute: int = 0
level_up_type: int = 0
level: int = 0
durability: int = 0
iuc: int = 0
exp: int = 0
grade: int = 0
chuc: int = 0
grade: int = 0
chuc: int = 0
option_1: int = 0
option_2: int = 0
option_3: int = 0
socket_1: int = 0
socket_2: int = 0
option_1: int = 0
option_2: int = 0
option_3: int = 0
socket_1: int = 0
socket_2: int = 0
lisn: int = 0
lisn: int = 0
storage_id: int = 0
sn: int = 0
sn: int = 0
def encode(self, packet):
packet.encode_byte(1)
@ -139,20 +141,20 @@ class ItemSlotEquip(ItemSlotBase):
packet.encode_short(self.option_3)
packet.encode_short(self.socket_1)
packet.encode_short(self.socket_2)
if not self.cisn:
packet.encode_long(0)
packet.encode_long(0)
packet.encode_int(0)
@dataclass
class ItemSlotBundle(ItemSlotBase):
number: int = 1
attribute: int = 0
lisn: int = 0
title: str = ""
number: int = 1
attribute: int = 0
lisn: int = 0
title: str = ""
def encode(self, packet):
packet.encode_byte(2)
@ -164,4 +166,4 @@ class ItemSlotBundle(ItemSlotBase):
packet.encode_short(self.attribute)
if self.item_id / 10000 == 207:
packet.encode_long(self.lisn)
packet.encode_long(self.lisn)

View File

@ -1,36 +1,39 @@
from dataclasses import dataclass, field
from typing import List
from common.abc import WildcardData
from mapy.common import WildcardData
class Skill:
def __init__(self, id):
self._id = id
self._skill_level_data = []
class SkillLevel:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
@dataclass
class SkillLevelData(WildcardData):
flags: List = field(default=list)
weapon: int = 0
sub_weapon: int = 0
max_level: int = 0
base_max_level: int = 0
skill_type: List = field(default=list)
element: str = ""
mob_count: str = ""
hit_count: str = ""
buff_time: str = ""
mp_cost: str = ""
hp_cost: str = ""
damage: str = ""
fixed_damage: str = ""
critical_damage: str = ""
mastery: str = ""
flags: List = field(default=list)
weapon: int = 0
sub_weapon: int = 0
max_level: int = 0
base_max_level: int = 0
skill_type: List = field(default=list)
element: str = ""
mob_count: str = ""
hit_count: str = ""
buff_time: str = ""
mp_cost: str = ""
hp_cost: str = ""
damage: str = ""
fixed_damage: str = ""
critical_damage: str = ""
mastery: str = ""
def __post_init__(self):
self._levels = {}
@ -42,18 +45,19 @@ class SkillLevelData(WildcardData):
pass
else:
kwargs[name] = value
self._levels[i] = SkillLevel(**kwargs)
def __getitem__(self, index):
return self._levels[index]
# @dataclass
# class SkillLevel(WildcardData):
# max_level:int = 0
# mob_count:int = 0
# hit_count:int = 0
# range:int = 0
# range:int = 0
# buff_time:int = 0
# cost_hp:int = 0
# cost_mp:int = 0

View File

@ -1,2 +1,2 @@
from .client import HTTPClient
from .server import HTTPServer
from .server import HTTPServer

111
mapy/http_api/client.py Normal file
View File

@ -0,0 +1,111 @@
from aiohttp import ClientSession
# from loguru import logger
from mapy.character import Character
from mapy.game import ItemSlotEquip
from mapy.common.constants import HTTP_API_ROUTE
from mapy.utils import fix_dict_keys
class Route:
def __init__(self, method, route, data=None, params=None, json=None):
self._method = method
self._route = route
self._data = data
self._params = params
self._json = json
class HTTPClient:
def __init__(self, provider=None):
self._host = provider | HTTP_API_ROUTE
async def request(self, route, *, content_type="json", **kwargs):
async with ClientSession() as session:
url = self._host + route._route
if route._data:
kwargs["data"] = route._data
if route._params:
kwargs["params"] = route._params
if route._json:
kwargs["json"] = route._json
r = await session.request(route._method, url, **kwargs)
try:
match [r.status, content_type]:
case [200, "json"]:
return await r.json()
case [200, "image"]:
return await r.read()
case [200, _]:
return await r.text()
case _:
return None
except Exception as e:
(e)
finally:
await r.release()
##
# Main Data Provider
##
async def is_username_taken(self, character_name):
response = await self.request(Route("GET", "/character/name/" + character_name))
return response["resp"]
async def login(self, username, password):
route = Route(
"POST",
"/login",
data={
"username": username,
"password": password,
},
)
response = await self.request(route)
return response
async def get_characters(self, account_id, world_id=None):
if not world_id:
url = f"/account/{account_id}/characters"
else:
url = f"/account/{account_id}/characters/{world_id}"
response = fix_dict_keys(await self.request(Route("GET", url)))
return [
Character.from_data(**character) for character in response["characters"]
]
async def create_new_character(self, account_id, character: Character):
route = Route(
"POST",
f"/account/{account_id}/character/new",
json=character.__serialize__(),
)
response = await self.request(route)
return response["id"]
##
# Static Data Provider
##
async def get_item(self, item_id):
route = Route("GET", f"/item/{item_id}")
response = await self.request(route)
item = ItemSlotEquip(**response)
return item

View File

@ -20,12 +20,7 @@ class Base(RouteTableDef):
path = handler._path
kwargs = handler._kwargs
self._items.append(
RouteDef(
method,
path,
getattr(self, handler.__name__),
kwargs
)
RouteDef(method, path, getattr(self, handler.__name__), kwargs)
)
@ -35,6 +30,7 @@ def route(method, path, **kwargs):
handler._path = path
handler._kwargs = kwargs
return handler
return wrap
@ -47,22 +43,24 @@ class Routes(Base):
@route("GET", "/")
async def get_status(self, request):
resp = {
'uptime': self._server.uptime,
'population': self._server.population,
'login_server': {
'alive': self._server.login.alive,
'port': self._server.login.port,
'population': self._server.login.population
"uptime": self._server.uptime,
"population": self._server.population,
"login_server": {
"alive": self._server.login.alive,
"port": self._server.login.port,
"population": self._server.login.population,
},
'game_servers': {
"game_servers": {
world.name: {
i: {
'alive': channel.alive,
'port': channel.port,
'population': channel.population
} for i, channel in enumerate(world.channels, 1)
} for world in self._server.worlds.values()
}
"alive": channel.alive,
"port": channel.port,
"population": channel.population,
}
for i, channel in enumerate(world.channels, 1)
}
for world in self._server.worlds.values()
},
}
return json_response(resp)

View File

@ -2,7 +2,7 @@ from aiohttp import web
from os import walk
import importlib
from utils import log
from mapy import log
class HTTPServer(web.Application):
@ -17,10 +17,8 @@ class HTTPServer(web.Application):
self.load_routes()
def load_routes(self):
self._routes = importlib.import_module('.routes', 'http_api')
self.router.add_routes(
self._routes.Routes(self)
)
self._routes = importlib.import_module(".routes", "mapy.http_api")
self.router.add_routes(self._routes.Routes(self))
def run(self):
runner = web.AppRunner(self)
@ -34,5 +32,5 @@ class HTTPServer(web.Application):
return self._server
def log(self, message, level=None):
level = level if level else 'debug'
level = level if level else "debug"
getattr(log, level)(f"{self._name} {message}")

99
mapy/logger.py Normal file
View File

@ -0,0 +1,99 @@
from re import IGNORECASE, compile, X
from .common.enum import Worlds
PACKET_RE = compile(
r"(?P<opcode>[A-Za-z0-9\._]+)\s(?P<ip>[0-9\.]+)\s(?P<packet>[A-Z0-9\s]*)"
)
SERVER_RE = compile(
r"""^
(?P<server>
(?P<name>[a-zA-Z]+\s?[a-zA-Z]+?)
(?P<game>
\[(?P<world_id>[0-9]+)\]
\[(?P<ch_id>[0-9]+)\]
)?
)\s
(?P<message>.+)$""",
flags=IGNORECASE | X,
)
try:
from loguru._logger import Logger as _Logger, Core
from sys import stdout
def fmt_packet(rec):
direction = in_ if rec["level"].name == (in_ := "INPACKET") else "OUTPACKET"
match_packet = PACKET_RE.search(rec["message"])
op_code, ip, packet = list(match_packet.group(1, 2, 3))
string = (
f"<lg>[</lg><level>{direction:^12}</level><lg>]</lg> "
f"<r>[</r><level>{op_code}</level><r>]</r> "
f"<g>[</g>{ip}<g>]</g> <w>{packet}</w>"
"\n"
)
return string
def fmt_record(record):
name, message = "Unnamed", ""
if grps := getattr(SERVER_RE.search(record["message"]), "group", None):
message = grps("message")
name = grps("name")
if grps("game"):
name = f"""{(
f"<lc>{Worlds(int(grps('world_id'))).name}"
f"</lc>: <lr>{int(grps('ch_id')) + 1}</lr>"): <33}"""
return (
"<lg>[</lg>"
f"<level>{record['level']: ^10}</level>"
"<lg>]</lg>"
f"<lg>[</lg>{name: <15}<lg>]</lg> "
f"<level>{message}</level>"
"\n"
)
def filter_packets(record):
return not record["level"] in ["INPACKET", "OUTPACKET"]
class Logger(_Logger):
def __init__(self):
super().__init__(
Core(), None, 0, False, False, False, False, True, None, {}
)
self.remove()
self.level("INPACKET", no=50, color="<c>")
self.level("OUTPACKET", no=50, color="<lm>")
self.add(
stdout,
format=fmt_record,
filter=filter_packets,
colorize=True,
diagnose=True,
)
self.add(
stdout,
level="INPACKET",
colorize=True,
format=fmt_packet,
)
self.add(
stdout,
level="OUTPACKET",
colorize=True,
format=fmt_packet,
)
def i_packet(self, message):
return self._log("INPACKET", None, False, self._options, message, (), {})
def o_packet(self, message):
return self._log("OUTPACKET", None, False, self._options, message, (), {})
log = Logger()
except ImportError:
from logging import getLogger
log = getLogger()

4
mapy/net/__init__.py Normal file
View File

@ -0,0 +1,4 @@
__all__ = "CSendOps", "CRecvOps", "Packet"
from .packet import Packet
from .opcodes import CSendOps, CRecvOps

View File

@ -0,0 +1,12 @@
__all__ = (
"MapleIV",
"MapleAes",
"decrypt_transform",
"encrypt_transform",
"roll_left",
"roll_right",
)
from .maple_iv import MapleIV
from .aes import MapleAes
from .shanda import decrypt_transform, encrypt_transform, roll_left, roll_right

View File

@ -1,18 +1,50 @@
from Crypto.Cipher import AES
try:
from Cryptodome.Cipher import AES
except ImportError:
from Crypto.Cipher import AES # type: ignore
except ImportError:
raise Exception("Please install pycryptodomex")
# 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
])
_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):
@ -42,9 +74,7 @@ class MapleAes:
sub = index - start
if (sub % 16) == 0:
real_iv = (
AES.new(cls._user_key, AES.MODE_ECB).encrypt(real_iv)
)
real_iv = AES.new(cls._user_key, AES.MODE_ECB).encrypt(real_iv)
buffer[index] ^= real_iv[sub % 16]
index += 1
@ -71,7 +101,7 @@ class MapleAes:
@staticmethod
def get_header(data, iv, length, major_ver):
first = -(major_ver + 1) ^ iv.hiword
second = (first + 2**16) ^ length
second = (first + 2 ** 16) ^ length
data[0:2] = bytes([first & 0xFF, first >> 8 & 0xFF])
data[2:4] = bytes([second & 0xFF, second >> 8 & 0xFF])

346
mapy/net/crypto/maple_iv.py Normal file
View File

@ -0,0 +1,346 @@
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
@property
def hiword(self):
return self.value >> 16
@property
def loword(self):
return self.value
# def shuffle(self):
# def to_int(arr):
# return (seed[0] & 0xFF) + (seed[1] << 8 & 0xFF)\
# + (seed[2] << 16 & 0xFF) + (seed[3] << 24 * 0xFF)
# def to_arr(int_):
# return [
# int_ & 0xFF,
# int_ >> 8 & 0xFF,
# int_ >> 16 & 0xFF,
# int_ >> 24 & 0xFF
# ]
# seed = [0xf2, 0x53, 0x50, 0xc6]
# p_iv = self.value
# for i in range (4):
# temp_iv = (p_iv >> (8 * i)) & 0xFF
# seed[0] += (self._shuffle[seed[1] & 0xFF]) - temp_iv
# seed[1] -= seed[2] ^ (self._shuffle[temp_iv])
# seed[2] ^= temp_iv + (self._shuffle[seed[3]])
# seed[3] = seed[3] - seed[0] + (self._shuffle[temp_iv])
# seed = to_arr((to_int(seed) << 3) | (to_int(seed) >> 29))
# self.value = to_int(seed)
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

View File

@ -31,6 +31,7 @@ def decrypt_transform(data):
return data
def encrypt_transform(data):
b = {str(i): 0 for i in range(len(data))}
cur = 0
@ -40,7 +41,7 @@ def encrypt_transform(data):
xor_key = 0
i = 0
while i < len(data):
cur = roll_left(data[i], 3)
cur = cur + length
cur = (cur ^ xor_key) & 0xFF
@ -52,7 +53,6 @@ def encrypt_transform(data):
length -= 1
i += 1
xor_key = 0
length = len(data) & 0xFF
i = len(data) - 1
@ -74,8 +74,9 @@ def encrypt_transform(data):
def roll_left(value, shift):
num = value << (shift % 8)
return ((num & 0xFF) | (num >> 8))
return (num & 0xFF) | (num >> 8)
def roll_right(value, shift):
num = (value << 8) >> (shift % 8)
return ((num & 0xFF) | (num >> 8))
return (num & 0xFF) | (num >> 8)

View File

@ -1,7 +1,9 @@
from enum import Enum
class OpCodes(Enum):
pass
...
class CRecvOps(OpCodes):
CP_CheckPassword = 0x1
@ -319,6 +321,7 @@ class CRecvOps(OpCodes):
CP_LogoutGiftSelect = 0x139
CP_NO = 0x13A
class CSendOps(OpCodes):
LP_CheckPasswordResult = 0x0
LP_GuestIDLoginResult = 0x1
@ -826,4 +829,4 @@ class CSendOps(OpCodes):
LP_Vega_e = 0x1AF
# LP_END_VEGA = 0x1AF
LP_LogoutGift = 0x1B0
LP_NO = 0x1B1
LP_NO = 0x1B1

View File

@ -2,22 +2,22 @@ from enum import Enum
from io import BytesIO
from struct import pack, unpack
from . import CRecvOps
from utils.tools import to_string
from .opcodes import CRecvOps
from mapy.utils.tools import to_string
# Junk codes for colorizing incoming packets from custom client
debug_codes = [
('r', ('|', '|')),
('lr', ('|', '&')),
('c', ('~', '~')),
('lc', ('~', '&')),
('y', ('#', '#')),
('ly', ('#', '&')),
('g', ('^', '^')),
('lg', ('^', '&')),
('m', ('@', '@')),
('lm', ('@', '&'))
("r", ("|", "|")),
("lr", ("|", "&")),
("c", ("~", "~")),
("lc", ("~", "&")),
("y", ("#", "#")),
("ly", ("#", "&")),
("g", ("^", "^")),
("lg", ("^", "&")),
("m", ("@", "@")),
("lm", ("@", "&")),
]
@ -44,15 +44,15 @@ class ByteBuffer(BytesIO):
return self
def encode_short(self, value):
self.write(pack('H', value))
self.write(pack("H", value))
return self
def encode_int(self, value):
self.write(pack('I', value))
self.write(pack("I", value))
return self
def encode_long(self, value):
self.write(pack('Q', value))
self.write(pack("Q", value))
return self
def encode_buffer(self, buffer):
@ -64,7 +64,7 @@ class ByteBuffer(BytesIO):
return self
def encode_string(self, string):
self.write(pack('H', len(string)))
self.write(pack("H", len(string)))
for ch in string:
self.write(ch.encode())
@ -82,7 +82,7 @@ class ByteBuffer(BytesIO):
return self
def encode_hex_string(self, string):
string = string.strip(' -')
string = string.strip(" -")
self.write(bytes.fromhex(string))
return self
@ -93,13 +93,13 @@ class ByteBuffer(BytesIO):
return bool(self.decode_byte())
def decode_short(self):
return unpack('H', self.read(2))[0]
return unpack("H", self.read(2))[0]
def decode_int(self):
return unpack('I', self.read(4))[0]
return unpack("I", self.read(4))[0]
def decode_long(self):
return unpack('Q', self.read(8))[0]
return unpack("Q", self.read(8))[0]
def decode_buffer(self, size):
return self.read(size)
@ -131,7 +131,7 @@ class Packet(ByteBuffer):
def __init__(self, data=None, op_code=None, raw=False):
if data == None:
data = b''
data = b""
super().__init__(data)
@ -172,7 +172,7 @@ class PacketHandler:
def __init__(self, name, callback, **kwargs):
self.name = name
self.callback = callback
self.op_code = kwargs.get('op_code')
self.op_code = kwargs.get("op_code")
def packet_handler(op_code=None):

View File

@ -1,2 +1,2 @@
from .context_base import ContextBase
from .script_base import ScriptBase
from .script_base import ScriptBase

View File

@ -1,3 +1,3 @@
class ContextBase:
def __init__(self, script):
self._script = script
self._script = script

View File

@ -0,0 +1 @@
from .npc_script import NpcScript

View File

@ -1 +1 @@
await ctx.say(f"Npc ID [{ctx.npc_id}] My script is not yet made")
await ctx.say(f"Npc ID [{ctx.npc_id}] My script is not yet made")

View File

@ -1,37 +1,39 @@
from dataclasses import dataclass
from scripts.context_base import ContextBase
from mapy.scripts.context_base import ContextBase
@dataclass
class Message:
msg: str = ""
prev: bool = False
nxt: bool = False
msg: str = ""
prev: bool = False
nxt: bool = False
def encode(self, packet):
packet.encode_string(self.msg)
packet.encode_byte(self.prev)
packet.encode_byte(self.nxt)
class NpcContext(ContextBase):
@property
def npc_id(self):
return self._script.npc_id
async def say(self, msg, prev=False, nxt=False):
self._script._prev_msgs.append(Message(msg, prev, nxt))
def action(packet):
packet.encode_string(msg)
packet.encode_byte(prev)
packet.encode_byte(nxt)
await self._script.send_message(0, action)
async def ask_yes_no(self, msg):
def action(packet):
packet.encode_string(msg)
await self._script.send_message(2, action)
def end_chat(self):
self._script.end_chat()

View File

@ -1,10 +1,9 @@
from asyncio import Queue
from os.path import isfile
from net.packets import CSendOps
from net import packets
from scripts import ScriptBase
from scripts.npc.npc_context import NpcContext
from mapy.net import CSendOps, Packet
from mapy.scripts.script_base import ScriptBase
from mapy.scripts.npc.npc_context import NpcContext
class NpcScript(ScriptBase):
@ -13,7 +12,7 @@ class NpcScript(ScriptBase):
script = f"scripts/npc/default.py"
else:
script = f"scripts/npc/{npc_id}.py"
super().__init__(script, client)
self._npc_id = npc_id
self._context = NpcContext(self)
@ -38,7 +37,7 @@ class NpcScript(ScriptBase):
return resp
async def send_dialogue(self, type_, action, flag, param):
packet = packets.Packet(op_code=CSendOps.LP_ScriptMessage)
packet = Packet(op_code=CSendOps.LP_ScriptMessage)
packet.encode_byte(flag)
packet.encode_int(self._npc_id)
packet.encode_byte(type_)
@ -48,14 +47,14 @@ class NpcScript(ScriptBase):
self._last_msg_type = type_
await self._parent.send_packet(packet)
async def reuse_dialogue(self, msg):
await self.send_dialogue(0, msg.encode, 4, 0)
async def proceed_back(self):
if self._prev_id == 0:
return
self._prev_id -= 1
await self.reuse_dialogue(self._prev_msgs[self._prev_id])
@ -65,7 +64,7 @@ class NpcScript(ScriptBase):
if self._prev_id < len(self._prev_msgs):
await self.reuse_dialogue(self._prev_msgs[self._prev_id])
else:
await self._response.put(resp)

View File

@ -0,0 +1,26 @@
class ScriptBase:
def __init__(self, script, client):
self._parent = client
self._script = compile(
"async def ex():\n"
+ "".join([f" {line}" for line in open(script, "r").readlines()]),
"<string>",
"exec",
)
self._context = None
async def execute(self):
async def run(script, _globals):
exec(script, _globals)
await _globals["ex"]()
env = {"ctx": self._context}
env.update(globals())
await run(self._script, env)
self._parent.npc_script = None
@property
def parent(self):
return self._parent

7
mapy/server/__init__.py Normal file
View File

@ -0,0 +1,7 @@
__all__ = "WvsCenter", "WvsLogin", "WvsGame", "World", "ServerBase"
from .server_base import ServerBase
from .wvs_center import WvsCenter
from .wvs_login import WvsLogin
from .wvs_game import WvsGame
from .world import World

View File

@ -1,15 +1,43 @@
from asyncio import Event, get_event_loop, get_running_loop, run_coroutine_threadsafe
from socket import AF_INET, IPPROTO_TCP, SOCK_STREAM, TCP_NODELAY, socket
from net.client import ClientSocket
from net.packets.packet import PacketHandler
from net.server import Dispatcher
from utils import log
from mapy.client.client_base import ClientSocket
from mapy.net.packet import PacketHandler
from mapy import log
class Dispatcher:
def __init__(self, parent):
self.parent = parent
def push(self, client, packet):
log.packet(
f"{self.parent.name} {packet.name} {client.ip} {packet.to_string()}", "in"
)
try:
coro = None
for packet_handler in self.parent._packet_handlers:
if packet_handler.op_code == packet.op_code:
coro = packet_handler.callback
break
if not coro:
raise AttributeError
except AttributeError:
log.warning(f"{self.parent.name} Unhandled event in : <w>{packet.name}</w>")
else:
self.parent._loop.create_task(self._run_event(coro, client, packet))
async def _run_event(self, coro, *args):
await coro(self.parent, *args)
class ServerBase:
"""Server base for center, channel, and login servers
"""
"""Server base for center, channel, and login servers"""
def __init__(self, parent, port):
self._loop = get_event_loop()
@ -18,8 +46,8 @@ class ServerBase:
self._is_alive = False
self._clients = []
self._packet_handlers = []
self._ready = Event(loop=self._loop)
self._alive = Event(loop=self._loop)
self._ready = Event()
self._alive = Event()
self._dispatcher = Dispatcher(self)
self._serv_sock = socket(AF_INET, SOCK_STREAM)
@ -49,7 +77,7 @@ class ServerBase:
async def on_client_accepted(self, socket):
client_sock = ClientSocket(socket)
client = await getattr(self, 'client_connect')(client_sock)
client = await getattr(self, "client_connect")(client_sock)
self.log(f"{self.name} Accepted <lg>{client.ip}</lg>")
self._clients.append(client)
@ -69,13 +97,14 @@ class ServerBase:
for _, member in members:
# Register all packet handlers for inheriting server
if (isinstance(member, PacketHandler)
and member not in self._packet_handlers):
if (
isinstance(member, PacketHandler)
and member not in self._packet_handlers
):
self._packet_handlers.append(member)
async def wait_until_ready(self) -> bool:
"""Block event loop until the GameServer has started listening for clients.
"""
"""Block event loop until the GameServer has started listening for clients."""
return await self._ready.wait()
async def listen(self):

View File

@ -1,10 +1,10 @@
from common import constants
from common.enum import WorldFlag, Worlds
from mapy import constants
from mapy.common.enum import WorldFlag, Worlds
class World:
def __init__(self, id):
self._world = Worlds(id)
self._shop_port = 9000
self._channels = []
self._flag = WorldFlag.New
self._allow_multi_leveling = constants.ALLOW_MULTI_LEVELING

View File

@ -3,17 +3,19 @@ from asyncio import get_event_loop
from configparser import ConfigParser
from time import time
from common.constants import (CHANNEL_COUNT, GAME_PORT)
from common.enum import Worlds
from db import DatabaseClient
from http_api import HTTPServer
from utils import log
from utils.tools import wakeup
from mapy import log
from mapy.common.constants import CHANNEL_COUNT, GAME_PORT
from mapy.common.enum import Worlds
from mapy.db import DatabaseClient
from mapy.http_api import HTTPServer
from mapy.utils import wakeup
from . import World, WvsGame, WvsLogin, WvsShop
from .world import World
from .wvs_game import WvsGame
from .wvs_login import WvsLogin
class ServerApp:
class WvsCenter:
_name = "Server Core"
"""Server connection listener for incoming client socket connections
@ -64,50 +66,55 @@ class ServerApp:
return self._worlds
def log(self, message, level=None):
level = level or 'info'
level = level or "info"
getattr(log, level)(f"{self._name} {message}")
def _load_config(self):
self._config = ConfigParser()
self._config.read('config.ini')
self._config.read("config.ini")
if len(self._config.sections()) < 1:
self._make_config()
def _make_config(self):
self.log("Please setup the database and other configuration")
self._config.add_section('database')
self._config['database']['user'] = input("DB User: ")
self._config['database']['password'] = input("DB Password: ")
self._config['database']['host'] = input("DB Host: ")
self._config['database']['port'] = input("DB Port: ")
self._config['database']['database'] = input("DB Name: ")
self._config.add_section("database")
self._config["database"]["user"] = input("DB User: ")
self._config["database"]["password"] = input("DB Password: ")
self._config["database"]["host"] = input("DB Host: ")
self._config["database"]["port"] = input("DB Port: ")
self._config["database"]["database"] = input("DB Name: ")
self._config.add_section('worlds')
self._config.add_section("worlds")
use_worlds = input("Setup worlds? (y/n) [Defaults will be used otherwise] ")
if use_worlds.lower() in ['y', 'yes']:
if use_worlds.lower() in ["y", "yes"]:
world_num = int(input("Number of worlds: [Max 20] "))
for i in range(world_num):
name = Worlds(i).name
self.log(f"Setting up {Worlds(i).name}...")
self._config['worlds'][name] = 'active'
self._config["worlds"][name] = "active"
self._config[name] = {}
self._config[name]['channels'] = input("Channels: ")
self._config[name]['exp_rate'] = input("Exp Rate: ")
self._config[name]['drop_rate'] = input("Drop Rate: ")
self._config[name]['meso_rate'] = input("Meso Rate: ")
self._config[name]["channels"] = input("Channels: ")
self._config[name]["exp_rate"] = input("Exp Rate: ")
self._config[name]["drop_rate"] = input("Drop Rate: ")
self._config[name]["meso_rate"] = input("Meso Rate: ")
else:
self._config['worlds']['Scania'] = 'active'
self._config['Scania'] = {'channels': 3, 'exp_rate': 1.0, 'drop_rate': 1.0, 'meso_rate': 1.0}
self._config["worlds"]["Scania"] = "active"
self._config["Scania"] = {
"channels": 3,
"exp_rate": 1.0,
"drop_rate": 1.0,
"meso_rate": 1.0,
}
with open("config.ini", "w") as config:
self._config.write(config)
@classmethod
def run(cls):
self = ServerApp()
self = WvsCenter()
self._http_api.run()
loop = self._loop
@ -139,14 +146,14 @@ class ServerApp:
self._start_time = int(time())
self.log("Initializing Server", "debug")
self.data = DatabaseClient(loop=self._loop, **self._config['database'])
self.data = DatabaseClient(loop=self._loop, **self._config["database"])
await self.data.start()
channel_port = GAME_PORT
self.login = await WvsLogin.run(self)
for world in self._config['worlds']:
if self._config['worlds'][world] != 'active':
for world in self._config["worlds"]:
if self._config["worlds"][world] != "active":
continue
world_id = Worlds[world.title()].value

View File

@ -1,18 +1,18 @@
from .server_base import ServerBase
from mapy.client import WvsGameClient
from mapy.common import Worlds
from mapy.common.constants import ANTIREPEAT_BUFFS, is_event_vehicle_skill
from mapy.net import CRecvOps
from mapy.net.packet import packet_handler
from mapy.scripts.npc import NpcScript
from mapy.utils.cpacket import CPacket
from client import WvsGameClient
from common.enum import Worlds
from common.constants import ANTIREPEAT_BUFFS, is_event_vehicle_skill
from net.packets import crypto, Packet, CRecvOps
from net.packets.packet import packet_handler
from scripts.npc import NpcScript
from utils import CPacket, log
from .server_base import ServerBase
class WvsGame(ServerBase):
def __init__(self, parent, port, world_id, channel_id):
super().__init__(parent, port)
self._name = f'Game Server[{world_id}][{channel_id}]'
self._name = f"Game Server[{world_id}][{channel_id}]"
self._field_manager = {}
self._allow_multi_leveling = False
@ -20,9 +20,9 @@ class WvsGame(ServerBase):
self.world_name = Worlds(world_id).name
self.channel_id = channel_id
self.ticker_message = "In game wow"
self.meso_rate = int(parent._config[self.world_name]['meso_rate'])
self.drop_rate = int(parent._config[self.world_name]['drop_rate'])
self.exp_rate = int(parent._config[self.world_name]['exp_rate'])
self.meso_rate = int(parent._config[self.world_name]["meso_rate"])
self.drop_rate = int(parent._config[self.world_name]["drop_rate"])
self.exp_rate = int(parent._config[self.world_name]["exp_rate"])
self.quest_exp_rate = 1
self.party_quest_exp_rate = 1
@ -79,18 +79,12 @@ class WvsGame(ServerBase):
login_req.character._client = client
client.character = await self.data.characters.load(uid, client)
await (
await self.get_field(client.character.field_id)
).add(client)
await (await self.get_field(client.character.field_id)).add(client)
await client.send_packet(
CPacket.claim_svr_changed(True))
await client.send_packet(
CPacket.set_gender(client.character.stats.gender))
await client.send_packet(
CPacket.func_keys_init(client.character.func_keys))
await client.send_packet(
CPacket.broadcast_server_msg(self.ticker_message))
await client.send_packet(CPacket.claim_svr_changed(True))
await client.send_packet(CPacket.set_gender(client.character.stats.gender))
await client.send_packet(CPacket.func_keys_init(client.character.func_keys))
await client.send_packet(CPacket.broadcast_server_msg(self.ticker_message))
@packet_handler(CRecvOps.CP_UserMove)
async def handle_user_move(self, client, packet):
@ -103,8 +97,7 @@ class WvsGame(ServerBase):
move_path = packet.decode_buffer(-1)
client.character.position.decode_move_path(move_path)
await client.broadcast(
CPacket.user_movement(client.character.id, move_path))
await client.broadcast(CPacket.user_movement(client.character.id, move_path))
@packet_handler(CRecvOps.CP_UserSkillUseRequest)
async def handle_skill_use_request(self, client, packet):
@ -134,12 +127,14 @@ class WvsGame(ServerBase):
if casted:
client.broadcast(
CPacket.effect_remote(client.character.obj_id,
1,
skill_id,
client.character.stats.level,
1)
CPacket.effect_remote(
client.character.obj_id,
1,
skill_id,
client.character.stats.level,
1,
)
)
@packet_handler(CRecvOps.CP_UserSelectNpc)
async def handle_user_select_npc(self, client, packet):
@ -148,7 +143,7 @@ class WvsGame(ServerBase):
packet.decode_short() # y
if client.npc_script:
pass # client has npc script already?
... # client has npc script already?
npc = client.character.field.npcs.get(obj_id)
@ -165,8 +160,8 @@ class WvsGame(ServerBase):
if type_ != type_expected:
self.log(
f"User answered type: [{type_}], expected [{type_expected}]"
"debug")
f"User answered type: [{type_}], expected [{type_expected}]" "debug"
)
return
resp = packet.decode_byte()
@ -182,8 +177,8 @@ class WvsGame(ServerBase):
@packet_handler(CRecvOps.CP_UpdateGMBoard)
async def handle_update_gm_board(self, client, packets):
pass
...
@packet_handler(CRecvOps.CP_RequireFieldObstacleStatus)
async def handle_require_field_obstacle(self, client, packets):
pass
...

View File

@ -1,20 +1,22 @@
import asyncio
from datetime import datetime
from client import WvsLoginClient
from character import Account, Character
from character.inventory import InventoryType
from common.constants import (
AUTO_LOGIN, AUTO_REGISTER, CENTER_PORT, HOST_IP, LOGIN_PORT,
MAX_CHARACTERS, REQUEST_PIC, REQUEST_PIN, REQUIRE_STAFF_IP, WORLD_COUNT,
get_job_from_creation)
from db import DatabaseClient
from net.packets import Packet
from net.packets.opcodes import CRecvOps, CSendOps
from net.packets.packet import packet_handler
from server.server_base import ServerBase
from utils.cpacket import CPacket
from mapy.client import WvsLoginClient
from mapy.character import Character
from mapy.common.constants import (
AUTO_LOGIN,
AUTO_REGISTER,
LOGIN_PORT,
MAX_CHARACTERS,
REQUEST_PIC,
REQUEST_PIN,
REQUIRE_STAFF_IP,
get_job_from_creation,
)
from mapy.net import Packet, CRecvOps
from mapy.net.packet import packet_handler
from mapy.server.server_base import ServerBase
from mapy.utils.cpacket import CPacket
class PendingLogin:
@ -68,10 +70,10 @@ class WvsLogin(ServerBase):
if response == 0:
return await client.send_packet(
CPacket.check_password_result(client, response))
CPacket.check_password_result(client, response)
)
await client.send_packet(
CPacket.check_password_result(response=response))
await client.send_packet(CPacket.check_password_result(response=response))
async def send_world_information(self, client) -> None:
for world in self._worlds:
@ -116,8 +118,7 @@ class WvsLogin(ServerBase):
name = packet.decode_string()
exists = await self.data.characters.get(name=name) is not None
await client.send_packet(
CPacket.check_duplicated_id_result(name, exists))
await client.send_packet(CPacket.check_duplicated_id_result(name, exists))
@packet_handler(CRecvOps.CP_ViewAllChar)
async def view_all_characters(self, client, packet):
@ -125,12 +126,10 @@ class WvsLogin(ServerBase):
packet.decode_byte() # game_start_mode
await asyncio.sleep(2)
await client.send_packet(
CPacket.start_view_all_characters(client.avatars))
await client.send_packet(CPacket.start_view_all_characters(client.avatars))
for world in self._worlds:
await client.send_packet(
CPacket.view_all_characters(world, client.avatars))
await client.send_packet(CPacket.view_all_characters(world, client.avatars))
@packet_handler(CRecvOps.CP_CreateNewCharacter)
async def create_new_character(self, client, packet):
@ -156,18 +155,19 @@ class WvsLogin(ServerBase):
character.gender = packet.decode_byte()
character_id = await self.data.account(id=client.account.id)\
.create_character(character)
character_id = await self.data.account(id=client.account.id).create_character(
character
)
if character_id:
character.id = character_id
client.avatars.append(character)
return await client.send_packet(
CPacket.create_new_character(character, False))
CPacket.create_new_character(character, False)
)
return await client.send_packet(
CPacket.create_new_character(character, True))
return await client.send_packet(CPacket.create_new_character(character, True))
@packet_handler(CRecvOps.CP_SelectCharacter)
async def select_character(self, client, packet):
@ -176,7 +176,7 @@ class WvsLogin(ServerBase):
port = self.parent.worlds[client.world_id][client.channel_id].port
self.parent._pending_logins.append(
PendingLogin(character, client.account, datetime.now()))
PendingLogin(character, client.account, datetime.now())
)
await client.send_packet(
CPacket.select_character_result(uid, port))
await client.send_packet(CPacket.select_character_result(uid, port))

6
mapy/server/wvs_shop.py Normal file
View File

@ -0,0 +1,6 @@
from mapy.server.server_base import ServerBase
class WvsShop(ServerBase):
def __init__(self):
pass

23
mapy/utils/__init__.py Normal file
View File

@ -0,0 +1,23 @@
__all__ = (
"get",
"to_string",
"wakeup",
"Manager",
"first_or_default",
"filter_out_to",
"fix_dict_keys",
"Random",
"TagPoint",
)
from .tools import (
Manager,
first_or_default,
wakeup,
filter_out_to,
fix_dict_keys,
get,
to_string,
)
from .random import Random
from .tag_point import TagPoint

View File

@ -1,13 +1,11 @@
from common import constants
from net import packets
from net.packets import CSendOps
from .tools import get
from mapy import constants
from mapy.net import Packet, CSendOps
class CPacket:
@staticmethod
def check_password_result(client=None, response=None):
packet = packets.Packet(op_code=CSendOps.LP_CheckPasswordResult)
packet = Packet(op_code=CSendOps.LP_CheckPasswordResult)
if response != 0:
packet.encode_int(response)
@ -40,7 +38,7 @@ class CPacket:
@staticmethod
def world_information(world):
packet = packets.Packet(op_code=CSendOps.LP_WorldInformation)
packet = Packet(op_code=CSendOps.LP_WorldInformation)
packet.encode_byte(world.id)
packet.encode_string(world.name)
packet.encode_byte(2) # 0 : Normal 1 : Event 2 : New 3 : Hot
@ -64,20 +62,20 @@ class CPacket:
@staticmethod
def end_world_information():
packet = packets.Packet(op_code=CSendOps.LP_WorldInformation)
packet = Packet(op_code=CSendOps.LP_WorldInformation)
packet.encode_byte(0xFF)
return packet
@staticmethod
def last_connected_world(world_id):
packet = packets.Packet(op_code=CSendOps.LP_LatestConnectedWorld)
packet = Packet(op_code=CSendOps.LP_LatestConnectedWorld)
# default: WorldID, 253: None, 255: Recommended World
packet.encode_int(world_id)
return packet
@staticmethod
def send_recommended_world(worlds):
packet = packets.Packet(op_code=CSendOps.LP_RecommendWorldMessage)
packet = Packet(op_code=CSendOps.LP_RecommendWorldMessage)
packet.encode_byte(len(worlds))
for world in worlds:
@ -88,7 +86,7 @@ class CPacket:
@staticmethod
def check_user_limit(status):
packet = packets.Packet(op_code=CSendOps.LP_CheckUserLimitResult)
packet = Packet(op_code=CSendOps.LP_CheckUserLimitResult)
# 0: Open 1: Over user limit
packet.encode_byte(0)
@ -98,7 +96,7 @@ class CPacket:
@staticmethod
def world_result(entries):
packet = packets.Packet(op_code=CSendOps.LP_SelectWorldResult)
packet = Packet(op_code=CSendOps.LP_SelectWorldResult)
packet.encode_byte(0)
packet.encode_byte(len(entries))
@ -114,19 +112,19 @@ class CPacket:
@staticmethod
def check_duplicated_id_result(name, is_available):
packet = packets.Packet(op_code=CSendOps.LP_CheckDuplicatedIDResult)
packet = Packet(op_code=CSendOps.LP_CheckDuplicatedIDResult)
packet.encode_string(name)
packet.encode_byte(is_available)
return packet
@staticmethod
def extra_char_info(character):
packet = packets.Packet(op_code=CSendOps.LP_CheckExtraCharInfoResult)
packet = Packet(op_code=CSendOps.LP_CheckExtraCharInfoResult)
return packet
@staticmethod
def start_view_all_characters(characters):
packet = packets.Packet(op_code=CSendOps.LP_ViewAllCharResult)
packet = Packet(op_code=CSendOps.LP_ViewAllCharResult)
packet.encode_byte(1)
packet.encode_int(2)
packet.encode_int(len(characters))
@ -134,16 +132,13 @@ class CPacket:
@staticmethod
def view_all_characters(world, characters):
packet = packets.Packet(op_code=CSendOps.LP_ViewAllCharResult)
packet = Packet(op_code=CSendOps.LP_ViewAllCharResult)
packet.encode_byte(0)
packet.encode_byte(world.id)
characters = list(
filter(
lambda character: character.world_id == world.id,
characters
)
filter(lambda character: character.world_id == world.id, characters)
)
packet.encode_byte(len(characters))
@ -159,7 +154,7 @@ class CPacket:
@staticmethod
def create_new_character(character, response: bool):
packet = packets.Packet(op_code=CSendOps.LP_CreateNewCharacterResult)
packet = Packet(op_code=CSendOps.LP_CreateNewCharacterResult)
packet.encode_byte(response)
if not response:
@ -169,10 +164,10 @@ class CPacket:
@staticmethod
def select_character_result(uid, port):
packet = packets.Packet(op_code=CSendOps.LP_SelectCharacterResult)
packet = Packet(op_code=CSendOps.LP_SelectCharacterResult)
packet.encode_byte(0) # world
packet.encode_byte(0) # selected char
packet.encode_byte(0) # world
packet.encode_byte(0) # selected char
packet.encode_buffer(constants.SERVER_ADDRESS)
packet.encode_short(port)
@ -186,7 +181,7 @@ class CPacket:
@staticmethod
def set_field(character, character_data, channel: int):
packet = packets.Packet(op_code=CSendOps.LP_SetField)
packet = Packet(op_code=CSendOps.LP_SetField)
# CPacket.cclient_opt_man__encode_opt(packet, 0)
packet.encode_short(0)
@ -208,39 +203,39 @@ class CPacket:
packet.encode_int(0)
packet.encode_int(0)
packet.encode_int(0)
else:
packet.encode_byte(0)
packet.encode_int(character.field_id)
packet.encode_byte(character.stats.portal)
packet.encode_int(character.stats.hp)
packet.encode_byte(0)
packet.encode_long(150842304000000000)
return packet
@staticmethod
def func_keys_init(keys):
packet = packets.Packet(op_code=CSendOps.LP_FuncKeyMappedInit)
packet = Packet(op_code=CSendOps.LP_FuncKeyMappedInit)
packet.encode_byte(0)
for i in range(90):
key = keys[i]
packet.encode_byte(getattr(key, 'type', 0))
packet.encode_int(getattr(key, 'action', 0))
packet.encode_byte(getattr(key, "type", 0))
packet.encode_int(getattr(key, "action", 0))
return packet
@staticmethod
def set_gender(gender):
packet = packets.Packet(op_code=CSendOps.LP_SetGender)
packet = Packet(op_code=CSendOps.LP_SetGender)
packet.encode_byte(gender)
return packet
@staticmethod
def stat_changed(modifier=None, excl_req=False):
packet = packets.Packet(op_code=CSendOps.LP_StatChanged)
packet = Packet(op_code=CSendOps.LP_StatChanged)
packet.encode_byte(excl_req)
if modifier:
modifier.encode(packet)
@ -257,7 +252,7 @@ class CPacket:
@staticmethod
def claim_svr_changed(claim_svr_con: bool):
packet = packets.Packet(op_code=CSendOps.LP_ClaimSvrStatusChanged)
packet = Packet(op_code=CSendOps.LP_ClaimSvrStatusChanged)
packet.encode_byte(claim_svr_con)
return packet
@ -265,93 +260,90 @@ class CPacket:
@staticmethod
def user_enter_field(character):
packet = packets.Packet(op_code=CSendOps.LP_UserEnterField)
packet = Packet(op_code=CSendOps.LP_UserEnterField)
packet.encode_int(character.id)
packet.encode_byte(character.stats.level)
packet.encode_string(character.stats.name)
packet.skip(8)
packet.encode_long(0)\
.encode_long(0)\
.encode_byte(0)\
.encode_byte(0)
packet.encode_long(0).encode_long(0).encode_byte(0).encode_byte(0)
packet.encode_short(character.stats.job)
character.encode_look(packet)
packet.encode_int(0) # driver ID
packet.encode_int(0) # passenger ID
packet.encode_int(0) # choco count
packet.encode_int(0) # active effeect item ID
packet.encode_int(0) # completed set item ID
packet.encode_int(0) # portable chair ID
packet.encode_int(0) # driver ID
packet.encode_int(0) # passenger ID
packet.encode_int(0) # choco count
packet.encode_int(0) # active effeect item ID
packet.encode_int(0) # completed set item ID
packet.encode_int(0) # portable chair ID
packet.encode_short(0) # private?
packet.encode_short(0) # private?
packet.encode_short(0)
packet.encode_byte(character.position.stance)
packet.encode_short(character.position.foothold)
packet.encode_byte(0) # show admin effect
packet.encode_byte(0) # show admin effect
packet.encode_byte(0) # pets?
packet.encode_byte(0) # pets?
packet.encode_int(0) # taming mob level
packet.encode_int(0) # taming mob exp
packet.encode_int(0) # taming mob fatigue
packet.encode_int(0) # taming mob level
packet.encode_int(0) # taming mob exp
packet.encode_int(0) # taming mob fatigue
packet.encode_byte(0) # mini room type
packet.encode_byte(0) # mini room type
packet.encode_byte(0) # ad board remote
packet.encode_byte(0) # on couple record add
packet.encode_byte(0) # on friend record add
packet.encode_byte(0) # on marriage record add
packet.encode_byte(0) # ad board remote
packet.encode_byte(0) # on couple record add
packet.encode_byte(0) # on friend record add
packet.encode_byte(0) # on marriage record add
packet.encode_byte(0) # some sort of effect bit flag
packet.encode_byte(0) # some sort of effect bit flag
packet.encode_byte(0) # new year card record add
packet.encode_int(0) # phase
packet.encode_byte(0) # new year card record add
packet.encode_int(0) # phase
return packet
@staticmethod
def user_leave_field(character):
packet = packets.Packet(op_code=CSendOps.LP_UserLeaveField)
packet = Packet(op_code=CSendOps.LP_UserLeaveField)
packet.encode_int(character.id)
return packet
@staticmethod
def user_movement(uid, move_path):
packet = packets.Packet(op_code=CSendOps.LP_UserMove)
packet = Packet(op_code=CSendOps.LP_UserMove)
packet.encode_int(uid)
packet.encode_buffer(move_path)
return packet
# --------------------- Mob Pool ------------------- #
@staticmethod
def mob_enter_field(mob):
packet = packets.Packet(op_code=CSendOps.LP_MobEnterField)
packet = Packet(op_code=CSendOps.LP_MobEnterField)
mob.encode_init(packet)
return packet
@staticmethod
def mob_change_controller(mob, level):
packet = packets.Packet(op_code=CSendOps.LP_MobChangeController)
packet = Packet(op_code=CSendOps.LP_MobChangeController)
packet.encode_byte(level)
if level == 0:
packet.encode_int(mob.obj_id)
else:
mob.encode_init(packet)
return packet
# --------------------- Npc Pool ----------------------#
@staticmethod
def npc_enter_field(npc):
packet = packets.Packet(op_code=CSendOps.LP_NpcEnterField)
packet = Packet(op_code=CSendOps.LP_NpcEnterField)
packet.encode_int(npc.obj_id)
packet.encode_int(npc.life_id)
@ -369,7 +361,7 @@ class CPacket:
@staticmethod
def npc_script_message(npc, msg_type, msg, end_bytes, type_, other_npc):
packet = packets.Packet(op_code=CSendOps.LP_ScriptMessage)
packet = Packet(op_code=CSendOps.LP_ScriptMessage)
packet.encode_byte(4)
packet.encode_int(npc)
@ -378,12 +370,12 @@ class CPacket:
if type_ in [4, 5]:
packet.encode_int(other_npc)
packet.encode_string(msg)
if end_bytes:
packet.encode(bytes(end_bytes))
return packet
@staticmethod
@ -392,11 +384,11 @@ class CPacket:
@staticmethod
def broadcast_msg(type_, msg):
packet = packets.Packet(op_code=CSendOps.LP_BroadcastMsg)
packet = Packet(op_code=CSendOps.LP_BroadcastMsg)
packet.encode_byte(type_)
if type_ == 4:
packet.encode_byte(True)
packet.encode_string(msg)
return packet
return packet

13
mapy/utils/random.py Normal file
View File

@ -0,0 +1,13 @@
from random import randint
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)

View File

@ -2,6 +2,6 @@ class TagPoint:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return f"{self.x},{self.y}"
return f"{self.x},{self.y}"

View File

@ -1,6 +1,19 @@
from asyncio import sleep
from dataclasses import dataclass, is_dataclass
from random import randint
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):
@ -9,10 +22,11 @@ def find(predicate, seq):
return element
return None
def get(iterable, **attrs):
def predicate(elem):
for attr, val in attrs.items():
nested = attr.split('__')
nested = attr.split("__")
obj = elem
for attribute in nested:
obj = getattr(obj, attribute)
@ -58,13 +72,14 @@ def fix_dict_keys(dict_):
def to_string(bytes_):
return ' '.join(
[bytes_.hex()[i:i+2].upper() for i in range(0, len(bytes_.hex()), 2)])
return " ".join(
[bytes_.hex()[i : i + 2].upper() for i in range(0, len(bytes_.hex()), 2)]
)
async def wakeup():
while True:
await sleep(.01)
await sleep(0.01)
def nested_dataclass(*args, **kwargs):
@ -94,4 +109,3 @@ class Manager(list):
def first_or_default(self, func):
return next((val for val in self if func(val)), None)

View File

@ -1 +0,0 @@
from .client_socket import ClientSocket

View File

@ -1,80 +0,0 @@
from io import BytesIO
from random import randint
from socket import SHUT_RD
from time import time
from utils.tools import to_string
from asyncio import create_task, Lock, get_event_loop
from common.constants import VERSION, SUB_VERSION, LOCALE
from net.packets.crypto import MapleIV, MapleAes, shanda
from net.packets.packet import Packet
class ClientSocket:
def __init__(self, socket):
self._loop = get_event_loop()
self._socket = socket
self._lock = Lock()
self.recieve_size = 16384
self.m_riv = None
self.m_siv = None
self._is_alive = False
self._overflow = None
@property
def identifier(self):
return self._socket.getpeername()
def close(self):
return self._socket.close()
async def receive(self, client):
self._is_alive = True
while self._is_alive:
if not self._overflow:
m_recv_buffer = await self._loop.sock_recv(self._socket, self.recieve_size)
if not m_recv_buffer:
await client._parent.on_client_disconnect(client)
return
else:
m_recv_buffer = self._overflow
self._overflow = None
if self.m_riv:
async with self._lock:
length = MapleAes.get_length(m_recv_buffer)
if length != len(m_recv_buffer) - 4:
self._overflow = m_recv_buffer[length + 4:]
m_recv_buffer = m_recv_buffer[:length + 4]
m_recv_buffer = self.manipulate_buffer(m_recv_buffer)
client.dispatch(Packet(m_recv_buffer))
async def send_packet(self, out_packet):
packet_length = len(out_packet)
packet = bytearray(out_packet.getvalue())
buf = packet[:]
final_length = packet_length + 4
final = bytearray(final_length)
async with self._lock:
MapleAes.get_header(final, self.m_siv, packet_length, VERSION)
buf = shanda.encrypt_transform(buf)
final[4:] = MapleAes.transform(buf, self.m_siv)
await self._loop.sock_sendall(self._socket, final)
async def send_packet_raw(self, packet):
await self._loop.sock_sendall(self._socket, packet.getvalue())
def manipulate_buffer(self, buffer):
buf = bytearray(buffer)[4:]
buf = MapleAes.transform(buf, self.m_riv)
buf = shanda.decrypt_transform(buf)
return buf

View File

@ -1,2 +0,0 @@
from .opcodes import CRecvOps, CSendOps
from .packet import Packet

View File

@ -1,2 +0,0 @@
from .maple_iv import MapleIV
from .aes import MapleAes

View File

@ -1,110 +0,0 @@
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
@property
def hiword(self):
return self.value >> 16
@property
def loword(self):
return self.value
# def shuffle(self):
# def to_int(arr):
# return (seed[0] & 0xFF) + (seed[1] << 8 & 0xFF)\
# + (seed[2] << 16 & 0xFF) + (seed[3] << 24 * 0xFF)
# def to_arr(int_):
# return [
# int_ & 0xFF,
# int_ >> 8 & 0xFF,
# int_ >> 16 & 0xFF,
# int_ >> 24 & 0xFF
# ]
# seed = [0xf2, 0x53, 0x50, 0xc6]
# p_iv = self.value
# for i in range (4):
# temp_iv = (p_iv >> (8 * i)) & 0xFF
# seed[0] += (self._shuffle[seed[1] & 0xFF]) - temp_iv
# seed[1] -= seed[2] ^ (self._shuffle[temp_iv])
# seed[2] ^= temp_iv + (self._shuffle[seed[3]])
# seed[3] = seed[3] - seed[0] + (self._shuffle[temp_iv])
# seed = to_arr((to_int(seed) << 3) | (to_int(seed) >> 29))
# self.value = to_int(seed)
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

View File

@ -1 +0,0 @@
from .dispatcher import Dispatcher

View File

@ -1,31 +0,0 @@
from utils import log
class Dispatcher:
def __init__(self, parent):
self.parent = parent
def push(self, client, packet):
log.packet(f"{self.parent.name} {packet.name} {client.ip} {packet.to_string()}", "in")
try:
coro = None
for packet_handler in self.parent._packet_handlers:
if packet_handler.op_code == packet.op_code:
coro = packet_handler.callback
break
if not coro:
raise AttributeError
except AttributeError:
log.warning(
f"{self.parent.name} Unhandled event in : <w>{packet.name}</w>")
else:
self.parent._loop.create_task(
self._run_event(coro, client, packet))
async def _run_event(self, coro, *args):
await coro(self.parent, *args)

9
run.py
View File

@ -1,8 +1,3 @@
from asyncio import get_event_loop
from sys import argv
from mapy import WvsCenter
from server.server import ServerApp
loop = get_event_loop()
ServerApp.run()
WvsCenter.run()

View File

@ -1 +0,0 @@
from .npc_script import NpcScript

View File

@ -1,33 +0,0 @@
from asyncio import create_task
from utils import log
class ScriptBase:
def __init__(self, script, client):
self._parent = client
self._script = compile(
"async def ex():\n" + "".join([f" {line}" for line in open(script, "r").readlines()]),
'<string>',
'exec'
)
self._context = None
@property
def parent(self):
return self._parent
async def execute(self):
async def run(script, _globals):
exec(script, _globals)
await _globals['ex']()
env = {
'ctx': self._context
}
env.update(globals())
await run(
self._script,
env
)
self._parent.npc_script = None

View File

@ -1,4 +0,0 @@
from .world import World
from .wvs_game import WvsGame
from .wvs_login import WvsLogin
from .wvs_shop import WvsShop

View File

@ -1,5 +0,0 @@
from server.server_base import ServerBase
class WvsShop(ServerBase):
def __init__(self):
pass

View File

@ -1,6 +1,8 @@
# a = [(z, i,) for z in [(a := 2), (2, 4,), (1, 2,), (3, 4,)] for i in ([1,2,3,4,5,6])]
a, b = 0, 0
for z, y in [(d := (a := 2**i - 1), ((b := a + (2 * i) & b) % 250)) for i in range(1, 20)]:
for z, y in [
(d := (a := 2 ** i - 1), ((b := a + (2 * i) & b) % 250)) for i in range(1, 20)
]:
while z > (c := b // 4):
z = (b := z**2-1) >> 0xFF & (y := a ^ b)
z = (b := z ** 2 - 1) >> 0xFF & (y := a ^ b)
print(z, c, b, y, a)

View File

@ -1,6 +0,0 @@
from .cpacket import CPacket
from .logger import logger as log
from .tools import Manager, first_or_default, wakeup, \
filter_out_to, fix_dict_keys, get, to_string
from .random import Random
from .tag_point import TagPoint

Some files were not shown because too many files have changed in this diff Show More