This commit is contained in:
Ra 2022-04-01 17:44:15 -07:00
parent c91aa653ce
commit 683f030d55
32 changed files with 3986 additions and 4352 deletions

29
.gitignore vendored

@ -1,15 +1,14 @@
# environment / pycache
**/.git
**/.vscode
**/.vs
**/*.pyc
**/__pycache__
**/.DS_STORE
**/.mypy_cache
# configs / tests
**/test*.*
**/notes.md
**/config.ini
# environment / pycache
**/.git
**/.vscode
**/.vs
**/*.pyc
**/__pycache__
**/.DS_STORE
**/.mypy_cache
**/notes.md
**/.*
**/config.*
**/test*.*
**/http_db_client.py

@ -1,15 +0,0 @@
[database]
user =
password =
host =
port =
database = maplestory
[worlds]
scania = active
[Scania]
channels = 3
exp_rate = 1
drop_rate = 2
meso_rate = 1

150
fm.md

@ -1,75 +1,75 @@
Free Market Integration
=======================
In the current version of MapleStory that we are working with, the free market is where all the items are sold by the players in the community amongst each other, and getting remotely close to viewing all listed items can take up to 4 hours within the game.
---
[Run Down](#run-down)
--------
*__Free Market Entrance__* | *__Free Market Room 1__*
-------------------------- | ----------------------
![Free Market Entrance](https://maplelegends.com/static/images/lib/map/910000000.png) | ![Free Market Room 1](https://maplelegends.com/static/images/lib/map/910000001.png)
Viewing the map page below can give a better example
----------------------------------------------------
[Map View](https://maplelegends.com/lib/map?id=910000000#3)
-----------------------------------------------------------
> - 24 Total Rooms
>
> - About 20 Shop locations per room (The closest another player can get to opening a shop near another shop requires that none of the shops image is overlapping onto another, leaving very limited space)
>
> - Players will often fight for the closest rooms to the entrace, as well as the closest location near the entrace to the room portal
>
> - All shops are closed and the rooms emptied once the server resets, so players will often plan for server resets in order to snipe the very best locations
>
> - As to not completely cut out the premium of having a good spot, a sort of stamina can be added to viewing within the web, where market search queries for an item listing will consume the most, manually searching through rooms the least (Still faster speed than in game however)
>
> - This also allows for the players that get the very last room [24] in channel 1 of the given world a fair chance to sell anything at all, as people can still find their listings within searches easily and be told their exact coords within the map
>
> - Knowing what the actual price of an item is can take weeks upon weeks of research, and prices often tend to fluctuate drastically, so having a listings page for past sales and statistics will hinder the market being 100% driven by players merchanting at 300% the normal cost
---
[Objectives](#objectives)
----------
[ ] Browseable Free market through mapy web
-----------------------------------------------
> **Using in-game assets as well as character model, walking around from room to room and interacting with actual players in game**
>
> **Steering away from entirely allowing player to buy on mapy web offers benefits to both players and hosts**
>
> - Promises to not entirely make getting optimal spots practically pointless for players who spend the time to plan for it
>
> - Doesn't entirely allow for players not have to worry at all about finding a competing location
>
> - Allowing for a couple purchases a day using [stamina](#stamina) could offer a way to allow players to still buy without spending multiple hours plus able to grab a deal if they are unable to get to their computer in time
>
> - Offer a way to further browse online using micro transactions to unlock features / usage time / allotted purchases
>
> - Allow players to still have access to social aspects of the game without having to have access to a computer, and allow full 24/7 access to the main entrance with no utilities if desired by players
[ ] Statistics View allowing searching of
-----------------------------------------
> - Average Quantity sold per day
>
> - Average price per x amount of a given item
>
> - Most Recently Sold quantity / price / time of the searched item
>
> - Daily / Weekly / Monthly change in price of an item
[ ] Cache all shops in an external redis service or something similar from within mapy
--------------------------------------------------------------------------
> - Quick access to all shops and sales
>
> - A safeguard to stop accidental closing of all shops due to an unforseen restart of the server which often times does not go over well
>
> - Easy Access for mapy web, however the option for a direct tcp connection to the server is still viable
Free Market Integration
=======================
In the current version of MapleStory that we are working with, the free market is where all the items are sold by the players in the community amongst each other, and getting remotely close to viewing all listed items can take up to 4 hours within the game.
---
[Run Down](#run-down)
--------
*__Free Market Entrance__* | *__Free Market Room 1__*
-------------------------- | ----------------------
![Free Market Entrance](https://maplelegends.com/static/images/lib/map/910000000.png) | ![Free Market Room 1](https://maplelegends.com/static/images/lib/map/910000001.png)
Viewing the map page below can give a better example
----------------------------------------------------
[Map View](https://maplelegends.com/lib/map?id=910000000#3)
-----------------------------------------------------------
> - 24 Total Rooms
>
> - About 20 Shop locations per room (The closest another player can get to opening a shop near another shop requires that none of the shops image is overlapping onto another, leaving very limited space)
>
> - Players will often fight for the closest rooms to the entrace, as well as the closest location near the entrace to the room portal
>
> - All shops are closed and the rooms emptied once the server resets, so players will often plan for server resets in order to snipe the very best locations
>
> - As to not completely cut out the premium of having a good spot, a sort of stamina can be added to viewing within the web, where market search queries for an item listing will consume the most, manually searching through rooms the least (Still faster speed than in game however)
>
> - This also allows for the players that get the very last room [24] in channel 1 of the given world a fair chance to sell anything at all, as people can still find their listings within searches easily and be told their exact coords within the map
>
> - Knowing what the actual price of an item is can take weeks upon weeks of research, and prices often tend to fluctuate drastically, so having a listings page for past sales and statistics will hinder the market being 100% driven by players merchanting at 300% the normal cost
---
[Objectives](#objectives)
----------
[ ] Browseable Free market through mapy web
-----------------------------------------------
> *__Using in-game assets as well as character model, walking around from room to room and interacting with actual players in game__*
>
> *__Steering away from entirely allowing player to buy on mapy web offers benefits to both players and hosts__*
>
> - Promises to not entirely make getting optimal spots practically pointless for players who spend the time to plan for it
>
> - Doesn't entirely allow for players not have to worry at all about finding a competing location
>
> - Allowing for a couple purchases a day using [stamina](#stamina) could offer a way to allow players to still buy without spending multiple hours plus able to grab a deal if they are unable to get to their computer in time
>
> - Offer a way to further browse online using micro transactions to unlock features / usage time / allotted purchases
>
> - Allow players to still have access to social aspects of the game without having to have access to a computer, and allow full 24/7 access to the main entrance with no utilities if desired by players
[ ] Statistics View allowing searching of
-----------------------------------------
> - Average Quantity sold per day
>
> - Average price per x amount of a given item
>
> - Most Recently Sold quantity / price / time of the searched item
>
> - Daily / Weekly / Monthly change in price of an item
[ ] Cache all shops in an external redis service or something similar from within mapy
--------------------------------------------------------------------------
> - Quick access to all shops and sales
>
> - A safeguard to stop accidental closing of all shops due to an unforseen restart of the server which often times does not go over well
>
> - Easy Access for mapy web, however the option for a direct tcp connection to the server is still viable

@ -1,7 +1,9 @@
__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
__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
print(log, type(log))

@ -1,245 +1,253 @@
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 .character_stats import CharacterStats
from .func_key import FuncKeys
from .inventory import InventoryManager, InventoryType
from .modifiers import CharacterModifiers
class Character(FieldObject):
def __init__(self, stats):
super().__init__()
self._client = None
self._data = None
self.stats = CharacterStats(**stats)
self.inventories = InventoryManager(self)
self.func_keys = FuncKeys(self)
self.modify = CharacterModifiers(self)
self.skills = {}
self.random = Random()
self.map_transfer = [0, 0, 0, 0, 0]
self.map_transfer_ex = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
self.monster_book_cover_id = 0
@property
def id(self):
return self.stats.id
@property
def field_id(self):
return self.stats.field_id
@property
def client(self):
return self._client
@client.setter
def client(self, value):
self._client = value
@property
def data(self):
return self._data
@data.setter
def data(self, value):
self._data = value
@property
def equip_inventory(self):
return self.inventories.get(1)
@property
def consume_inventory(self):
return self.inventories.get(2)
@property
def install_inventory(self):
return self.inventories.get(3)
@property
def etc_inventory(self):
return self.inventories.get(4)
@property
def cash_inventory(self):
return self.inventories.get(5)
def encode_entry(self, packet):
ranking = False
self.stats.encode(packet)
self.encode_look(packet)
packet.encode_byte(0)
packet.encode_byte(0)
if ranking:
packet.skip(16)
def encode(self, packet):
packet.encode_long(-1 & 0xFFFFFFFF)
packet.encode_byte(0) # combat orders
packet.encode_byte(0)
self.stats.encode(packet)
packet.encode_byte(100) # Buddylist capacity
packet.encode_byte(False)
packet.encode_int(self.stats.money)
self.encode_inventories(packet)
self.encode_skills(packet)
self.encode_quests(packet)
self.encode_minigames(packet)
self.encode_rings(packet)
self.encode_teleports(packet)
# self.encode_monster_book(packet)
self.encode_new_year(packet)
packet.encode_short(0)
# self.encode_area(packet)
packet.encode_short(0)
packet.encode_short(0)
def encode_inventories(self, packet):
packet.encode_byte(self.equip_inventory.slots)
packet.encode_byte(self.consume_inventory.slots)
packet.encode_byte(self.install_inventory.slots)
packet.encode_byte(self.etc_inventory.slots)
packet.encode_byte(self.cash_inventory.slots)
packet.encode_int(0)
packet.encode_int(0)
equipped = {}
for index, item in self.equip_inventory.items.items():
if index < 0:
equipped[index] = self.equip_inventory[index]
stickers, eqp_normal = {}, {}
if equipped.get(-11):
eqp_normal[-11] = equipped.pop(-11)
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
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():
if not item:
continue
packet.encode_short(abs(slot))
item.encode(packet)
packet.encode_short(0)
self.consume_inventory.encode(packet)
self.install_inventory.encode(packet)
self.etc_inventory.encode(packet)
self.cash_inventory.encode(packet)
def encode_skills(self, packet):
packet.encode_short(len(self.skills))
for _, skill in self.skills.items():
skill.encode(packet)
if False:
packet.encode_int(skill.mastery_level) # is skill needed for mastery
packet.encode_short(0)
def encode_quests(self, packet):
packet.encode_short(0)
packet.encode_short(0)
def encode_minigames(self, packet):
packet.encode_short(0)
def encode_rings(self, packet):
packet.encode_short(0)
packet.encode_short(0)
packet.encode_short(0)
# Maybe needs to not be filled by default
def encode_teleports(self, packet):
for _ in range(5):
packet.encode_int(0)
for _ in range(10):
packet.encode_int(0)
def encode_monster_book(self, packet):
packet.encode_int(self.monster_book_cover_id)
packet.encode_byte(0)
packet.encode_short(0)
def encode_new_year(self, packet):
packet.encode_short(0)
def encode_area(self, packet):
packet.encode_short(0)
def encode_look(self, packet):
packet.encode_byte(self.stats.gender)
packet.encode_byte(self.stats.skin)
packet.encode_int(self.stats.face)
packet.encode_byte(0)
packet.encode_int(self.stats.hair)
inventory = self.inventories.get(InventoryType.EQUIP)
equipped = {}
for index, item in inventory:
if index < 0:
equipped[index] = inventory[index]
stickers, unseen = {}, {}
for index, item in equipped.items():
if index > -100 and equipped.get(index - 100):
unseen[index] = item
else:
new_index = index + 100 if index < -100 else index
stickers[new_index] = item
for inv in [stickers, unseen]:
for index, item in inv.items():
packet.encode_byte(index * -1).encode_int(item.item_id)
packet.encode_byte(0xFF)
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)
async def send_packet(self, packet):
await self._client.send_packet(packet)
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 .character_stats import CharacterStats
from .func_key import FuncKeys
from .inventory import Inventory, InventoryManager, InventoryType
from .modifiers import CharacterModifiers
class Character(FieldObject):
def __init__(self, stats):
super().__init__()
self._client = None
self._data = None
self.stats = CharacterStats(**stats)
self.inventories: InventoryManager = InventoryManager(self)
self.func_keys = FuncKeys(self)
self.modify = CharacterModifiers(self)
self.skills = {}
self.random = Random()
self.map_transfer = [0, 0, 0, 0, 0]
self.map_transfer_ex = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
self.monster_book_cover_id = 0
@property
def id(self):
return self.stats.id
@property
def field_id(self):
return self.stats.field_id
@property
def client(self):
return self._client
@client.setter
def client(self, value):
self._client = value
@property
def data(self):
return self._data
@data.setter
def data(self, value):
self._data = value
@property
def equip_inventory(self) -> Inventory | None:
return self.inventories.get(1)
@property
def consume_inventory(self) -> Inventory | None:
return self.inventories.get(2)
@property
def install_inventory(self) -> Inventory | None:
return self.inventories.get(3)
@property
def etc_inventory(self) -> Inventory | None:
return self.inventories.get(4)
@property
def cash_inventory(self) -> Inventory | None:
return self.inventories.get(5)
def encode_entry(self, packet):
ranking = False
self.stats.encode(packet)
self.encode_look(packet)
packet.encode_byte(0)
packet.encode_byte(0)
if ranking:
packet.skip(16)
def encode(self, packet):
packet.encode_long(-1 & 0xFFFFFFFF)
packet.encode_byte(0) # combat orders
packet.encode_byte(0)
self.stats.encode(packet)
packet.encode_byte(100) # Buddylist capacity
packet.encode_byte(False)
packet.encode_int(self.stats.money)
self.encode_inventories(packet)
self.encode_skills(packet)
self.encode_quests(packet)
self.encode_minigames(packet)
self.encode_rings(packet)
self.encode_teleports(packet)
# self.encode_monster_book(packet)
self.encode_new_year(packet)
packet.encode_short(0)
# self.encode_area(packet)
packet.encode_short(0)
packet.encode_short(0)
def encode_inventories(self, packet):
packet.encode_byte(self.equip_inventory.slots)
packet.encode_byte(self.consume_inventory.slots)
packet.encode_byte(self.install_inventory.slots)
packet.encode_byte(self.etc_inventory.slots)
packet.encode_byte(self.cash_inventory.slots)
packet.encode_int(0)
packet.encode_int(0)
equipped = {}
for index, item in self.equip_inventory.items.items():
if index < 0:
equipped[index] = self.equip_inventory[index]
stickers, eqp_normal = {}, {}
if equipped.get(-11):
eqp_normal[-11] = equipped.pop(-11)
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
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():
if not item:
continue
packet.encode_short(abs(slot))
item.encode(packet)
packet.encode_short(0)
self.consume_inventory.encode(packet)
self.install_inventory.encode(packet)
self.etc_inventory.encode(packet)
self.cash_inventory.encode(packet)
def encode_skills(self, packet):
packet.encode_short(len(self.skills))
for _, skill in self.skills.items():
skill.encode(packet)
if False:
packet.encode_int(
skill.mastery_level
) # is skill needed for mastery
packet.encode_short(0)
def encode_quests(self, packet):
packet.encode_short(0)
packet.encode_short(0)
def encode_minigames(self, packet):
packet.encode_short(0)
def encode_rings(self, packet):
packet.encode_short(0)
packet.encode_short(0)
packet.encode_short(0)
# Maybe needs to not be filled by default
def encode_teleports(self, packet):
for _ in range(5):
packet.encode_int(0)
for _ in range(10):
packet.encode_int(0)
def encode_monster_book(self, packet):
packet.encode_int(self.monster_book_cover_id)
packet.encode_byte(0)
packet.encode_short(0)
def encode_new_year(self, packet):
packet.encode_short(0)
def encode_area(self, packet):
packet.encode_short(0)
def encode_look(self, packet):
packet.encode_byte(self.stats.gender)
packet.encode_byte(self.stats.skin)
packet.encode_int(self.stats.face)
packet.encode_byte(0)
packet.encode_int(self.stats.hair)
inventory: Inventory = self.inventories.get(InventoryType.EQUIP)
equipped = {}
for index, item in inventory:
if index < 0:
equipped[index] = inventory[index]
stickers, unseen = {}, {}
for index, item in equipped.items():
if index > -100 and equipped.get(index - 100):
unseen[index] = item
else:
new_index = index + 100 if index < -100 else index
stickers[new_index] = item
for inv in [stickers, unseen]:
for index, item in inv.items():
packet.encode_byte(index * -1).encode_int(item.item_id)
packet.encode_byte(0xFF)
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)
async def send_packet(self, packet):
await self._client.send_packet(packet)

@ -1,91 +1,92 @@
from dataclasses import dataclass, field
from typing import List
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(WildcardData):
id: int = 0
name: str = "Maple Char"
world_id: int = 0
gender: int = 0
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
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)
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)
def encode(self, packet) -> None:
packet.encode_int(self.id)
packet.encode_fixed_string(self.name, 13)
packet.encode_byte(self.gender)
packet.encode_byte(self.skin)
packet.encode_int(self.face)
packet.encode_int(self.hair)
for sn in self.pet_locker:
packet.encode_long(sn)
packet.encode_byte(self.level)
packet.encode_short(self.job)
packet.encode_short(self.str)
packet.encode_short(self.dex)
packet.encode_short(self.int)
packet.encode_short(self.luk)
packet.encode_int(self.hp)
packet.encode_int(self.m_hp)
packet.encode_int(self.mp)
packet.encode_int(self.m_mp)
packet.encode_short(self.ap)
# if player not evan
packet.encode_short(self.sp)
# else
# packet.encode_byte(len(self.extend_sp))
# for i, sp in enumerate(self.extend_sp):
# packet.encode_byte(i)
# packet.encode_byte(sp)
packet.encode_int(self.exp)
packet.encode_short(self.fame)
packet.encode_int(self.temp_exp)
packet.encode_int(self.field_id)
packet.encode_byte(self.portal)
packet.encode_int(self.play_time)
packet.encode_short(self.sub_job)
from dataclasses import dataclass, field
from typing import List
from attrs import define
import attr
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]
@define(kw_only=True, init=True, auto_attribs=True)
class CharacterStats(object):
id: int = 0
name: str = "Maple Char"
world_id: int = 0
gender: int = 0
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
hp: int = 50
m_hp: int = 50
mp: int = 5
m_mp: int = 5
ap: int = 0
sp: int = 0
extend_sp: list = []
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 = []
def encode(self, packet) -> None:
packet.encode_int(self.id)
packet.encode_fixed_string(self.name, 13)
packet.encode_byte(self.gender)
packet.encode_byte(self.skin)
packet.encode_int(self.face)
packet.encode_int(self.hair)
for sn in self.pet_locker:
packet.encode_long(sn)
packet.encode_byte(self.level)
packet.encode_short(self.job)
packet.encode_short(self.str_)
packet.encode_short(self.dex)
packet.encode_short(self.int_)
packet.encode_short(self.luk)
packet.encode_int(self.hp)
packet.encode_int(self.m_hp)
packet.encode_int(self.mp)
packet.encode_int(self.m_mp)
packet.encode_short(self.ap)
# if player not evan
packet.encode_short(self.sp)
# else
# packet.encode_byte(len(self.extend_sp))
# for i, sp in enumerate(self.extend_sp):
# packet.encode_byte(i)
# packet.encode_byte(sp)
packet.encode_int(self.exp)
packet.encode_short(self.fame)
packet.encode_int(self.temp_exp)
packet.encode_int(self.field_id)
packet.encode_byte(self.portal)
packet.encode_int(self.play_time)
packet.encode_short(self.sub_job)

@ -1,137 +1,153 @@
from mapy.common import Inventory as _Inventory, InventoryType
from mapy.game import item as Item
class InventoryManager:
def __init__(self, character):
self._character = character
self.tracker = Tracker()
self.inventories = {}
for i in range(1, 6):
self.inventories[i] = Inventory(InventoryType(i), 96)
def __iter__(self):
return ((item[0], item[1].items) for item in self.inventories.items())
@property
def updates(self):
return self.tracker.inventory_changes
def get(self, inventory_type):
if isinstance(inventory_type, InventoryType):
return self.inventories[inventory_type.value]
elif isinstance(inventory_type, int):
return self.inventories.get(inventory_type)
return None
def add(self, item, slot=0):
item_type = int(item.item_id / 1000000)
inventory = self.inventories[item_type]
slots = inventory.add(item, slot)
for slot, item in slots:
self.tracker.insert(item, slot)
class Tracker:
def __init__(self):
self.type = InventoryType.TRACKER
self._starting = []
# Only update this on stat improvement,
# movement, or new item added
self._items = {i: {} for i in range(1, 6)}
def insert(self, item, slot):
self._items[int(item.item_id / 1000000)][slot] = item
@property
def inventory_changes(self):
return [
{**item.__dict__, "inventory_type": inv_type, "position": slot}
for inv_type, inventory in self._items.items()
for slot, item in inventory.items()
]
# def get_update(self):
# return [{**item.__dict__,
# 'inventory_type': inv_type,
# 'position': slot
# } for inv_type, inventory in self._items.items()
# for slot, item in inventory.items()]
def get_throwaway(self):
throwaway = []
for _, inv in self._items.items():
for _, item in inv.items():
if item is None or item.inventory_item_id in self._starting:
continue
throwaway.append(item.inventory_item_id)
return throwaway
def copy(self, *inventories):
for _, inventory in inventories:
for _, item in inventory.items():
if item:
self._starting.append(item.inventory_item_id)
class Inventory(_Inventory):
def __init__(self, type_, slots):
self._unique_id = None
self.type = type_
self.items = {i: None for i in range(1, slots + 1)}
self._slots = slots
def __getitem__(self, key):
return self.items.get(key)
def __iter__(self):
return (item for item in self.items.items())
def get_free_slot(self):
for i in range(1, self._slots + 1):
if not self.items[i]:
return i
return None
def add(self, item, slot=None):
if isinstance(item, Item.ItemSlotEquip):
free_slot = self.get_free_slot() if not slot else slot
if free_slot:
self.items[free_slot] = item
items = ((free_slot, item),)
else:
items = None
elif isinstance(item, Item.ItemSlotBundle):
# Get Slot with same item_id and not max bundle
# or insert into free slot
pass
return items
@property
def slots(self):
return self._slots
def encode(self, packet):
for slot, item in self.items.items():
if not item:
continue
packet.encode_byte(slot)
item.encode(packet)
packet.encode_byte(0)
from typing import Any
from mapy.common import Inventory as _Inventory, InventoryType
from mapy.game import item as Item
from .inventory import Inventory
class InventoryManager:
def __init__(self, character):
self._character = character
self.tracker = Tracker()
self.inventories: dict[int, Inventory] = {}
for i in range(1, 6):
self.inventories[i] = Inventory(InventoryType(i), 96)
def __iter__(self):
return ((inv_type, inv.items)
for inv_type, inv in self.inventories.items())
@property
def updates(self):
return self.tracker.inventory_changes
def get(self, inventory_type):
if isinstance(inventory_type, InventoryType):
return self.inventories[inventory_type.value]
elif isinstance(inventory_type, int):
return self.inventories.get(inventory_type)
return None
def add(self, item: Item.ItemSlotBase, slot=0):
item_type = int(item.item_id / 1000000)
inventory: Inventory = self.inventories[item_type]
item_ = inventory.add(item, slot)
if not item_:
return
slot = item_[0]
item_ = item_[1]
self.tracker.insert(slot, item_)
class Tracker:
def __init__(self):
self.type = InventoryType.TRACKER
self._starting = []
# Only update this on stat improvement,
# movement, or new item added
self._items = {i: {} for i in range(1, 6)}
def insert(self, item, slot):
self._items[int(item.item_id / 1000000)][slot] = item
@property
def inventory_changes(self):
return [{
**item.__dict__, "inventory_type": inv_type,
"position": slot
}
for inv_type, inventory in self._items.items()
for slot, item in inventory.items()]
# def get_update(self):
# return [{**item.__dict__,
# 'inventory_type': inv_type,
# 'position': slot
# } for inv_type, inventory in self._items.items()
# for slot, item in inventory.items()]
def get_throwaway(self):
throwaway = []
for _, inv in self._items.items():
for _, item in inv.items():
if item is None or item.inventory_item_id in self._starting:
continue
throwaway.append(item.inventory_item_id)
return throwaway
def copy(self, *inventories):
for _, inventory in inventories:
for _, item in inventory.items():
if item:
self._starting.append(item.inventory_item_id)
class Inventory(_Inventory):
def __init__(self, type_, slots):
self._unique_id = None
self.type = type_
self.items: dict[int, Any] = {i: None for i in range(1, slots + 1)}
self._slots = slots
def __getitem__(self, key):
return self.items.get(key)
def __iter__(self):
return (item for item in self.items)
def get_free_slot(self):
for i in range(1, self._slots + 1):
if not self.items[i]:
return i
return None
def add(self,
item: Item.ItemSlotBase,
slot=None) -> tuple[int, Item.ItemSlotBase | None]:
items = None
if isinstance(item, Item.ItemSlotEquip):
free_slot = self.get_free_slot() if not slot else slot
if free_slot:
self.items[free_slot] = item
items = (free_slot, item)
elif isinstance(item, Item.ItemSlotBundle):
# Get Slot with same item_id and not max bundle
# or insert into free slot
pass
if not items:
return (0, None)
return items
@property
def slots(self):
return self._slots
def encode(self, packet):
for slot, item in self.items.items():
if not item:
continue
packet.encode_byte(slot)
item.encode(packet)
packet.encode_byte(0)

@ -1,137 +1,133 @@
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
from asyncio import Lock, get_event_loop
from random import randint
from mapy import log
from mapy.common.constants import LOCALE, SUB_VERSION, VERSION
from mapy.net.crypto import (
MapleAes, MapleIV, decrypt_transform, encrypt_transform
)
from mapy.net.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 = bytearray(packet_length + 4)
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_riv = MapleIV(randint(0, 2**31 - 1))
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

@ -1,51 +1,55 @@
from abc import ABCMeta
class Serializable(metaclass=ABCMeta):
def __serialize__(self):
serialized = {}
for key, value in self.__dict__.items():
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():
if key not in dir(cls):
continue
cleaned[key] = value
old_init(self, *args, **cleaned)
cls.__init__ = _new_init_
return super(WildcardData, cls).__new__(cls)
class Inventory:
...
# def add(self, item, slot=None):
# if isinstance(item, ItemSlotEquip):
# free_slot = self.get_free_slot() if not slot else slot
# if free_slot:
# self.items[free_slot] = item
# items = ((free_slot, item),)
# else:
# items = None
# elif isinstance(item, ItemSlotBundle):
# # Get Slot with same item_id and not max bundle
# # or insert into free slot
# pass
# return items
from abc import ABCMeta
from typing import Any
class Serializable(metaclass=ABCMeta):
def __serialize__(self):
serialized = {}
for key, value in self.__dict__.items():
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():
if key not in dir(cls):
continue
cleaned[key] = value
old_init(self, *args, **cleaned)
cls.__init__ = _new_init_
return super(WildcardData, cls).__new__(cls)
class Inventory:
items: dict[int | None, Any] | None
def add(self, item, slot=None):
raise NotImplementedError
# def add(self, item, slot=None):
# if isinstance(item, ItemSlotEquip):
# free_slot = self.get_free_slot() if not slot else slot
# if free_slot:
# self.items[free_slot] = item
# items = ((free_slot, item),)
# else:
# items = None
# elif isinstance(item, ItemSlotBundle):
# # Get Slot with same item_id and not max bundle
# # or insert into free slot
# pass
# return items

@ -1,117 +1,121 @@
HOST_IP = "127.0.0.1"
SERVER_ADDRESS = bytes([127, 0, 0, 1])
CENTER_PORT = 8383
LOGIN_PORT = 8484
GAME_PORT = 8585
# if there are 20 worlds with 20 channels they will collide with 8787, future thought
SHOP_PORT = 8787
USE_DATABASE = True
# DB_HOST = ""
# DB_PASS = ""
# DSN = "postgres://user:password@host:port/database"
USE_HTTP_API = False
HTTP_API_ROUTE = "http://127.0.0.1:54545"
WORLD_COUNT = 1
CHANNEL_COUNT = 2
VERSION = 95
SUB_VERSION = "1"
LOCALE = 8
EXP_RATE = 1
QUEST_EXP = 1
PARTY_QUEST_EXP = 1
MESO_RATE = 1
DROP_RATE = 1
LOG_PACKETS = True
AUTO_LOGIN = False
AUTO_REGISTER = True
REQUEST_PIN = False
REQUEST_PIC = False
REQUIRE_STAFF_IP = False
MAX_CHARACTERS = 3
DEFAULT_EVENT_MESSAGE = "Wow amazing world choose this one"
DEFAULT_TICKER = "Welcome"
ALLOW_MULTI_LEVELING = False
DEFAULT_CREATION_SLOTS = 3
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,
]
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,
]
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
HOST_IP = "127.0.0.1"
SERVER_ADDRESS = bytes([127, 0, 0, 1])
CENTER_PORT = 8383
LOGIN_PORT = 8584
GAME_PORT = 8585
# if there are 20 worlds with 20 channels they will collide with 8787, future thought
SHOP_PORT = 8787
USE_DATABASE = True
# DB_HOST = ""
# DB_PASS = ""
# DSN = "postgres://user:password@host:port/database"
USE_HTTP_API = False
HTTP_API_ROUTE = "http://127.0.0.1:54545"
WORLD_COUNT = 1
CHANNEL_COUNT = 6
VERSION = 112
SUB_VERSION = "4"
LOCALE = 7
# VERSION = 95
# SUB_VERSION = "1"
# LOCALE = 8
EXP_RATE = 1
QUEST_EXP = 1
PARTY_QUEST_EXP = 1
MESO_RATE = 1
DROP_RATE = 1
LOG_PACKETS = True
AUTO_LOGIN = False
AUTO_REGISTER = True
REQUEST_PIN = False
REQUEST_PIC = False
REQUIRE_STAFF_IP = False
MAX_CHARACTERS = 3
DEFAULT_EVENT_MESSAGE = "Wow amazing world choose this one"
DEFAULT_TICKER = "Welcome"
ALLOW_MULTI_LEVELING = False
DEFAULT_CREATION_SLOTS = 3
DISABLE_CHARACTER_CREATION = True
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,
]
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,
]
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

@ -1,76 +1,78 @@
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
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):
encode: str
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 +1,3 @@
from .db_client import DatabaseClient, Account
# from .http_db_client import HTTPClient

File diff suppressed because it is too large Load Diff

@ -1,284 +1,284 @@
import pydoc
import datetime
import decimal
import inspect
from .errors import SchemaError
class SQLType:
python = type(None)
def to_dict(self):
dct = self.__dict__.copy()
clas = self.__class__
dct["__meta__"] = clas.__module__ + "." + clas.__qualname__
return dct
@classmethod
def from_dict(cls, data):
meta = data.pop("__meta__")
given = cls.__module__ + "." + cls.__qualname__
if given != meta:
cls = pydoc.locate(meta)
if cls is None:
raise RuntimeError(f'Could not locate "{meta}".')
self = cls.__new__(cls)
self.__dict__.update(data)
return self
def __eq__(self, other):
return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
def __ne__(self, other):
return not self.__eq__(other)
def to_sql(self):
raise NotImplementedError()
def is_real_type(self):
return True
class Boolean(SQLType):
python = bool
def to_sql(self):
return "BOOLEAN"
class Date(SQLType):
python = datetime.date
def to_sql(self):
return "DATE"
class Datetime(SQLType):
python = datetime.datetime
def __init__(self, *, timezone=False):
self.timezone = timezone
def to_sql(self):
if self.timezone:
return "TIMESTAMP WITH TIMEZONE"
return "TIMESTAMP"
class Double(SQLType):
python = float
def to_sql(self):
return "REAL"
class Integer(SQLType):
python = int
def __init__(self, *, big=False, small=False, auto_increment=False):
self.big = big
self.small = small
self.auto_increment = auto_increment
if 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"
if self.small:
return "SMALLSERIAL"
return "SERIAL"
if self.big:
return "BIGINT"
if self.small:
return "SMALLINT"
return "INTEGER"
def is_real_type(self):
return not self.auto_increment
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",
)
def __init__(self, field=None):
if field:
field = field.upper()
if field not in self.valid_fields:
raise SchemaError("invalid interval specified")
self.field = field
def to_sql(self):
if self.field:
return "INTERVAL " + self.field
return "INTERVAL"
class Decimal(SQLType):
python = decimal.Decimal
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")
if scale is None:
scale = 0
self.precision = precision
self.scale = scale
def to_sql(self):
if self.precision is not None:
return f"NUMERIC({self.precision}, {self.scale})"
return "NUMERIC"
class Numeric(SQLType):
python = decimal.Decimal
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")
if scale is None:
scale = 0
self.precision = precision
self.scale = scale
def to_sql(self):
if self.precision is not None:
return f"NUMERIC({self.precision}, {self.scale})"
return "NUMERIC"
class String(SQLType):
python = str
def __init__(self, *, length=None, fixed=False):
self.length = length
self.fixed = fixed
if fixed and length is None:
raise SchemaError("Cannot have fixed string with no length")
def to_sql(self):
if self.length is None:
return "TEXT"
if self.fixed:
return f"CHAR({self.length})"
return f"VARCHAR({self.length})"
class Time(SQLType):
python = datetime.time
def __init__(self, *, timezone=False):
self.timezone = timezone
def to_sql(self):
if self.timezone:
return "TIME WITH TIME ZONE"
return "TIME"
class JSON(SQLType):
python = None
def to_sql(self):
return "JSONB"
class ForeignKey(SQLType):
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)")
valid_actions = (
"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)
if on_update not in valid_actions:
raise TypeError("on_update must be one of %s." % valid_actions)
self.table = table
self.column = column
self.on_update = on_update
self.on_delete = on_delete
if sql_type is None:
sql_type = Integer
if inspect.isclass(sql_type):
sql_type = sql_type()
if not isinstance(sql_type, SQLType):
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')
self.sql_type = sql_type.to_sql()
def is_real_type(self):
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}"
)
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")
self.type = inner_type
self.size = size
def to_sql(self):
if self.size:
return f"{self.type.to_sql()}[{self.size}]"
return f"{self.type.to_sql()}[]"
import pydoc
import datetime
import decimal
import inspect
from .errors import SchemaError
class SQLType:
python = type(None)
def to_dict(self):
dct = self.__dict__.copy()
clas = self.__class__
dct["__meta__"] = clas.__module__ + "." + clas.__qualname__
return dct
@classmethod
def from_dict(cls, data):
meta = data.pop("__meta__")
given = cls.__module__ + "." + cls.__qualname__
if given != meta:
cls = pydoc.locate(meta)
if cls is None:
raise RuntimeError(f'Could not locate "{meta}".')
self = cls.__new__(cls)
self.__dict__.update(data)
return self
def __eq__(self, other):
return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
def __ne__(self, other):
return not self.__eq__(other)
def to_sql(self):
raise NotImplementedError()
def is_real_type(self):
return True
class Boolean(SQLType):
python = bool
def to_sql(self):
return "BOOLEAN"
class Date(SQLType):
python = datetime.date
def to_sql(self):
return "DATE"
class Datetime(SQLType):
python = datetime.datetime
def __init__(self, *, timezone=False):
self.timezone = timezone
def to_sql(self):
if self.timezone:
return "TIMESTAMP WITH TIMEZONE"
return "TIMESTAMP"
class Double(SQLType):
python = float
def to_sql(self):
return "REAL"
class Integer(SQLType):
python = int
def __init__(self, *, big=False, small=False, auto_increment=False):
self.big = big
self.small = small
self.auto_increment = auto_increment
if 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"
if self.small:
return "SMALLSERIAL"
return "SERIAL"
if self.big:
return "BIGINT"
if self.small:
return "SMALLINT"
return "INTEGER"
def is_real_type(self):
return not self.auto_increment
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",
)
def __init__(self, field=None):
if field:
field = field.upper()
if field not in self.valid_fields:
raise SchemaError("invalid interval specified")
self.field = field
def to_sql(self):
if self.field:
return "INTERVAL " + self.field
return "INTERVAL"
class Decimal(SQLType):
python = decimal.Decimal
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")
if scale is None:
scale = 0
self.precision = precision
self.scale = scale
def to_sql(self):
if self.precision is not None:
return f"NUMERIC({self.precision}, {self.scale})"
return "NUMERIC"
class Numeric(SQLType):
python = decimal.Decimal
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")
if scale is None:
scale = 0
self.precision = precision
self.scale = scale
def to_sql(self):
if self.precision is not None:
return f"NUMERIC({self.precision}, {self.scale})"
return "NUMERIC"
class String(SQLType):
python = str
def __init__(self, *, length=None, fixed=False):
self.length = length
self.fixed = fixed
if fixed and length is None:
raise SchemaError("Cannot have fixed string with no length")
def to_sql(self):
if self.length is None:
return "TEXT"
if self.fixed:
return f"CHAR({self.length})"
return f"VARCHAR({self.length})"
class Time(SQLType):
python = datetime.time
def __init__(self, *, timezone=False):
self.timezone = timezone
def to_sql(self):
if self.timezone:
return "TIME WITH TIME ZONE"
return "TIME"
class JSON(SQLType):
python = None
def to_sql(self):
return "JSONB"
class ForeignKey(SQLType):
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)")
valid_actions = (
"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)
if on_update not in valid_actions:
raise TypeError("on_update must be one of %s." % valid_actions)
self.table = table
self.column = column
self.on_update = on_update
self.on_delete = on_delete
if sql_type is None:
sql_type = Integer
if inspect.isclass(sql_type):
sql_type = sql_type()
if not isinstance(sql_type, SQLType):
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')
self.sql_type = sql_type.to_sql()
def is_real_type(self):
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}"
)
class ArraySQL(SQLType):
def __init__(self, inner_type, size: int | None = None):
if not isinstance(inner_type, SQLType):
raise SchemaError("Array inner type must be an SQLType")
self.type = inner_type
self.size = size
def to_sql(self):
if self.size:
return f"{self.type.to_sql()}[{self.size}]"
return f"{self.type.to_sql()}[]"

@ -1,140 +1,142 @@
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
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

@ -1,169 +1,169 @@
from dataclasses import dataclass
from enum import Enum
from typing import List
from mapy.common import WildcardData
class ItemInventoryTypes(Enum):
ItemSlotEquip = 0x1
@dataclass
class ItemSlotBase(WildcardData):
"""Base item class for all items
Parameters
----------
item_id: int
Item temaplte ID
cisn: int
Cash Inventory Serial Numer
Used for tracking cash items
expire: :class:`datetime.datetime`
Expiry date of the item, if any
inventory_item_id: int
Primary key to store the item in database
flag: bool
Determines whether item has been deleted,
transfered, or stayed in inventory
"""
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`
The packet to encode the data onto
"""
packet.encode_int(self.item_id)
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
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
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
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
lisn: int = 0
storage_id: int = 0
sn: int = 0
def encode(self, packet):
packet.encode_byte(1)
super().encode(packet)
packet.encode_byte(self.ruc)
packet.encode_byte(self.cuc)
packet.encode_short(self.str)
packet.encode_short(self.dex)
packet.encode_short(self.int)
packet.encode_short(self.luk)
packet.encode_short(self.hp)
packet.encode_short(self.mp)
packet.encode_short(self.weapon_attack)
packet.encode_short(self.magic_attack)
packet.encode_short(self.weapon_defense)
packet.encode_short(self.magic_defense)
packet.encode_short(self.accuracy)
packet.encode_short(self.avoid)
packet.encode_short(self.craft)
packet.encode_short(self.speed)
packet.encode_short(self.jump)
packet.encode_string(self.title)
packet.encode_short(self.attribute)
packet.encode_byte(self.level_up_type)
packet.encode_byte(self.level)
packet.encode_int(self.exp)
packet.encode_int(-1 & 0xFFFFFF)
packet.encode_int(self.iuc)
packet.encode_byte(self.grade)
packet.encode_byte(self.chuc)
packet.encode_short(self.option_1)
packet.encode_short(self.option_2)
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 = ""
def encode(self, packet):
packet.encode_byte(2)
super().encode(packet)
packet.encode_short(self.number)
packet.encode_string(self.title)
packet.encode_short(self.attribute)
if self.item_id / 10000 == 207:
packet.encode_long(self.lisn)
from dataclasses import dataclass
from enum import Enum
import attr
from mapy.common import WildcardData
class ItemInventoryTypes(Enum):
ItemSlotEquip = 0x1
@attr.s(auto_attribs=True, init=True)
class ItemSlotBase(object):
"""Base item class for all items
Parameters
----------
item_id: int
Item temaplte ID
cisn: int
Cash Inventory Serial Numer
Used for tracking cash items
expire: :class:`datetime.datetime`
Expiry date of the item, if any
inventory_item_id: int
Primary key to store the item in database
flag: bool
Determines whether item has been deleted,
transfered, or stayed in inventory
"""
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`
The packet to encode the data onto
"""
packet.encode_int(self.item_id)
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[int] | None = 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
weapon_defense: 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
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
option_1: int = 0
option_2: int = 0
option_3: int = 0
socket_1: int = 0
socket_2: int = 0
lisn: int = 0
storage_id: int = 0
sn: int = 0
def encode(self, packet):
packet.encode_byte(1)
super().encode(packet)
packet.encode_byte(self.ruc)
packet.encode_byte(self.cuc)
packet.encode_short(self.str_)
packet.encode_short(self.dex)
packet.encode_short(self.int_)
packet.encode_short(self.luk)
packet.encode_short(self.hp)
packet.encode_short(self.mp)
packet.encode_short(self.weapon_attack)
packet.encode_short(self.magic_attack)
packet.encode_short(self.weapon_defense)
packet.encode_short(self.magic_defense)
packet.encode_short(self.accuracy)
packet.encode_short(self.avoid)
packet.encode_short(self.craft)
packet.encode_short(self.speed)
packet.encode_short(self.jump)
packet.encode_string(self.title)
packet.encode_short(self.attribute)
packet.encode_byte(self.level_up_type)
packet.encode_byte(self.level)
packet.encode_int(self.exp)
packet.encode_int(-1 & 0xFFFFFF)
packet.encode_int(self.iuc)
packet.encode_byte(self.grade)
packet.encode_byte(self.chuc)
packet.encode_short(self.option_1)
packet.encode_short(self.option_2)
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 = ""
def encode(self, packet):
packet.encode_byte(2)
super().encode(packet)
packet.encode_short(self.number)
packet.encode_string(self.title)
packet.encode_short(self.attribute)
if self.item_id / 10000 == 207:
packet.encode_long(self.lisn)

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

@ -1,111 +0,0 @@
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

@ -1,66 +0,0 @@
from aiohttp.web import RouteDef, RouteTableDef, json_response
class Base(RouteTableDef):
def __init_subclass__(cls):
cls._handlers = []
for k, v in cls.__dict__.items():
if k.startswith("_"):
continue
cls._handlers.append(v)
def __new__(cls, http_serv):
new_cls = super().__new__(cls)
return new_cls
def __init__(self):
super().__init__()
for handler in self._handlers:
method = handler._method
path = handler._path
kwargs = handler._kwargs
self._items.append(
RouteDef(method, path, getattr(self, handler.__name__), kwargs)
)
def route(method, path, **kwargs):
def wrap(handler):
handler._method = method
handler._path = path
handler._kwargs = kwargs
return handler
return wrap
class Routes(Base):
def __init__(self, http_serv):
self._http = http_serv
self._server = http_serv.server
super().__init__()
@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,
},
"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()
},
}
return json_response(resp)

@ -1,36 +1,98 @@
from aiohttp import web
from os import walk
import importlib
from mapy import log
class HTTPServer(web.Application):
def __init__(self, server_core, port=None, loop=None):
self._name = "HTTP API"
self._server = server_core
self._loop = server_core._loop
self._port = port
self._routes = None
super().__init__(loop=self._loop)
self.load_routes()
def load_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)
self.loop.run_until_complete(runner.setup())
site = web.TCPSite(runner, port=self._port)
self.loop.run_until_complete(site.start())
self.log(f"Listening on port <lr>{self._port}</lr>", "info")
@property
def server(self):
return self._server
def log(self, message, level=None):
level = level if level else "debug"
getattr(log, level)(f"{self._name} {message}")
import importlib
from aiohttp import web
from aiohttp.web import RouteDef, RouteTableDef, json_response
from mapy import log
class Base(RouteTableDef):
def __init_subclass__(cls):
cls._handlers = []
for k, v in cls.__dict__.items():
if k.startswith("_"):
continue
cls._handlers.append(v)
def __new__(cls, http_serv):
new_cls = super().__new__(cls)
return new_cls
def __init__(self):
super().__init__()
for handler in self._handlers:
method = handler._method
path = handler._path
kwargs = handler._kwargs
self._items.append(
RouteDef(method, path, getattr(self, handler.__name__), kwargs))
def route(method, path, **kwargs):
def wrap(handler):
handler._method = method
handler._path = path
handler._kwargs = kwargs
return handler
return wrap
class Routes(Base):
def __init__(self, http_serv):
self._http = http_serv
self._server = http_serv.server
super().__init__()
@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,
},
"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()
},
}
return json_response(resp)
class HTTPServer(web.Application):
def __init__(self, server_core, port=None, loop=None):
self._name = "HTTP API"
self._server = server_core
self._loop = server_core._loop
self._port = port
self._routes = None
super().__init__(loop=self._loop)
self.router.add_routes(Routes(self))
def run(self):
runner = web.AppRunner(self)
self.loop.run_until_complete(runner.setup())
site = web.TCPSite(runner, port=self._port)
self.loop.run_until_complete(site.start())
self.log(f"Listening on port <lr>{self._port}</lr>", "info")
@property
def server(self):
return self._server
def log(self, message, level=None):
getattr(log, level or "debug")(f"{self._name} {message}")

@ -1,99 +1,100 @@
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()
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 packet(self, message, direction):
return self._log(direction.upper() + "PACKET", None, False,
self._options, message, {}, {})
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()

@ -1,346 +1,84 @@
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
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):
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

File diff suppressed because it is too large Load Diff

BIN
mapy/net/packet.7z Normal file

Binary file not shown.

@ -1,182 +1,181 @@
from enum import Enum
from io import BytesIO
from struct import pack, unpack
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", ("@", "&")),
]
class DebugType(Enum):
_byte = 0x1
_short = 0x2
_int = 0x4
_long = 0x8
_string = 0x10
class ByteBuffer(BytesIO):
"""Base class for packet write and read operations"""
def encode(self, _bytes):
self.write(_bytes)
return self
def encode_byte(self, value):
if isinstance(value, Enum):
value = value.value
self.write(bytes([value]))
return self
def encode_short(self, value):
self.write(pack("H", value))
return self
def encode_int(self, value):
self.write(pack("I", value))
return self
def encode_long(self, value):
self.write(pack("Q", value))
return self
def encode_buffer(self, buffer):
self.write(buffer)
return self
def skip(self, count):
self.write(bytes(count))
return self
def encode_string(self, string):
self.write(pack("H", len(string)))
for ch in string:
self.write(ch.encode())
return self
def encode_fixed_string(self, string, length):
for i in range(13):
if i < len(string):
self.write(string[i].encode())
continue
self.encode_byte(0)
return self
def encode_hex_string(self, string):
string = string.strip(" -")
self.write(bytes.fromhex(string))
return self
def decode_byte(self):
return self.read(1)[0]
def decode_bool(self):
return bool(self.decode_byte())
def decode_short(self):
return unpack("H", self.read(2))[0]
def decode_int(self):
return unpack("I", self.read(4))[0]
def decode_long(self):
return unpack("Q", self.read(8))[0]
def decode_buffer(self, size):
return self.read(size)
def decode_string(self):
length = self.decode_short()
string = ""
for _ in range(length):
string += self.read(1).decode()
return string
class Packet(ByteBuffer):
"""Packet class use in all send / recv opertions
Parameters
----------
data: bytes
The initial data to load into the packet
op_code: :class:`OpCodes`
OpCode used to encode the first short onto the packet
op_codes: :class:`OpCodes`
Which enum to try to get the op_code from
"""
def __init__(self, data=None, op_code=None, raw=False):
if data == None:
data = b""
super().__init__(data)
if not data:
self.op_code = op_code
if isinstance(self.op_code, int):
self.encode_short(self.op_code)
else:
self.encode_short(self.op_code.value)
return
if raw:
return
self.op_code = CRecvOps(self.decode_short())
@property
def name(self):
if isinstance(self.op_code, int):
return self.op_code
return self.op_code.name
def to_array(self):
return self.getvalue()
def to_string(self):
return to_string(self.getvalue())
def __len__(self):
return len(self.getvalue())
class PacketHandler:
def __init__(self, name, callback, **kwargs):
self.name = name
self.callback = callback
self.op_code = kwargs.get("op_code")
def packet_handler(op_code=None):
def wrap(func):
return PacketHandler(func.__name__, func, op_code=op_code)
return wrap
from enum import Enum
from io import BytesIO
from struct import pack, unpack
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", ("@", "&")),
]
class DebugType(Enum):
_byte = 0x1
_short = 0x2
_int = 0x4
_long = 0x8
_string = 0x10
class ByteBuffer(BytesIO):
"""Base class for packet write and read operations"""
def encode(self, _bytes):
self.write(_bytes)
return self
def encode_byte(self, value):
if isinstance(value, Enum):
value = value.value
self.write(bytes([value]))
return self
def encode_short(self, value):
self.write(pack("H", value))
return self
def encode_int(self, value):
self.write(pack("I", value))
return self
def encode_long(self, value):
self.write(pack("Q", value))
return self
def encode_buffer(self, buffer):
self.write(buffer)
return self
def skip(self, count):
self.write(bytes(count))
return self
def encode_string(self, string):
self.write(pack("H", len(string)))
for ch in string:
self.write(ch.encode())
return self
def encode_fixed_string(self, string, length):
for i in range(13):
if i < len(string):
self.write(string[i].encode())
continue
self.encode_byte(0)
return self
def encode_hex_string(self, string):
string = string.strip(" -")
self.write(bytes.fromhex(string))
return self
def decode_byte(self):
return self.read(1)[0]
def decode_bool(self):
return bool(self.decode_byte())
def decode_short(self):
return unpack("H", self.read(2))[0]
def decode_int(self):
return unpack("I", self.read(4))[0]
def decode_long(self):
return unpack("Q", self.read(8))[0]
def decode_buffer(self, size):
return self.read(size)
def decode_string(self):
length = self.decode_short()
string = ""
for _ in range(length):
string += self.read(1).decode()
return string
class Packet(ByteBuffer):
"""Packet class use in all send / recv opertions
Parameters
----------
data: bytes
The initial data to load into the packet
op_code: :class:`OpCodes`
OpCode used to encode the first short onto the packet
op_codes: :class:`OpCodes`
Which enum to try to get the op_code from
"""
def __init__(self, data=None, op_code=None, raw=False):
if data == None:
data = b""
super().__init__(data)
if not data:
self.op_code = op_code
if isinstance(self.op_code, int):
self.encode_short(self.op_code)
else:
self.encode_short(self.op_code.value)
return
if not raw:
self.op_code = CRecvOps(self.decode_short())
@property
def name(self):
if isinstance(self.op_code, int):
return self.op_code
return self.op_code.name
def to_array(self):
return self.getvalue()
def to_string(self):
return to_string(self.getvalue())
def __len__(self):
return len(self.getvalue())
class PacketHandler:
def __init__(self, name, callback, **kwargs):
self.name = name
self.callback = callback
self.op_code = kwargs.get("op_code")
def packet_handler(op_code=None):
def wrap(func):
return PacketHandler(func.__name__, func, op_code=op_code)
return wrap

@ -1,78 +1,82 @@
from asyncio import Queue
from os.path import isfile
from mapy.net import CSendOps, Packet
from mapy.scripts.script_base import ScriptBase
from mapy.scripts.npc.npc_context import NpcContext
class NpcScript(ScriptBase):
def __init__(self, npc_id, client, default=False):
if default:
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)
self._last_msg_type = None
self._prev_msgs = []
self._prev_id = 0
self._response = Queue(maxsize=1)
@property
def npc_id(self):
return self._npc_id
@property
def last_msg_type(self):
return self._last_msg_type
async def send_message(self, type_, action, flag=4, param=0):
await self.send_dialogue(type_, action, flag, param)
resp = await self._response.get()
return resp
async def send_dialogue(self, type_, action, flag, param):
packet = Packet(op_code=CSendOps.LP_ScriptMessage)
packet.encode_byte(flag)
packet.encode_int(self._npc_id)
packet.encode_byte(type_)
packet.encode_byte(param)
action(packet)
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])
async def proceed_next(self, resp):
self._prev_id += 1
if self._prev_id < len(self._prev_msgs):
await self.reuse_dialogue(self._prev_msgs[self._prev_id])
else:
await self._response.put(resp)
def end_chat(self):
self.parent.npc_script = None
@staticmethod
def get_script(npc_id, client):
if isfile(f"scripts/npc/{npc_id}.py"):
return NpcScript(npc_id, client)
return NpcScript(npc_id, client, default=True)
from asyncio import Queue
from os.path import isfile
from mapy.net import CSendOps, Packet
from mapy.scripts.script_base import ScriptBase
from mapy.scripts.npc.npc_context import NpcContext
class NpcScript(ScriptBase):
def __init__(self, npc_id, client, default=False):
if default:
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)
self._last_msg_type = None
self._prev_msgs = []
self._prev_id = 0
self._response = Queue(maxsize=1)
@property
def npc_id(self):
return self._npc_id
@property
def last_msg_type(self):
return self._last_msg_type
async def send_message(self, type_, action, flag=4, param=0):
await self.send_dialogue(type_, action, flag, param)
resp = await self._response.get()
return resp
async def send_dialogue(self, type_, action, flag, param):
packet = Packet(op_code=CSendOps.LP_ScriptMessage)
packet.encode_byte(flag)
packet.encode_int(self._npc_id)
packet.encode_byte(type_)
packet.encode_byte(param)
action(packet)
self._last_msg_type = type_
self._response.task_done()
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])
async def proceed_next(self, resp):
self._prev_id += 1
if self._prev_id < len(self._prev_msgs):
await self.reuse_dialogue(self._prev_msgs[self._prev_id])
else:
self._response.task_done()
await self._response.put(resp)
def end_chat(self):
self.parent.npc_script = None
self._response.task_done()
@staticmethod
def get_script(npc_id, client):
if isfile(f"scripts/npc/{npc_id}.py"):
return NpcScript(npc_id, client)
return NpcScript(npc_id, client, default=True)

@ -1,141 +1,143 @@
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 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"""
def __init__(self, parent, port):
self._loop = get_event_loop()
self._parent = parent
self._port = port
self._is_alive = False
self._clients = []
self._packet_handlers = []
self._ready = Event()
self._alive = Event()
self._dispatcher = Dispatcher(self)
self._serv_sock = socket(AF_INET, SOCK_STREAM)
self._serv_sock.setblocking(0)
self._serv_sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
self._serv_sock.bind(("127.0.0.1", self._port))
self._serv_sock.listen(0)
self.add_packet_handlers()
def log(self, message, level=None):
level = level or "info"
getattr(log, level)(f"{self._name} {message}")
@property
def alive(self):
return self._alive.is_set()
async def start(self):
self._is_alive = True
self._alive.set()
self._ready.set()
self._listener = self._loop.create_task(self.listen())
def close(self):
self._listener.cancel()
async def on_client_accepted(self, socket):
client_sock = ClientSocket(socket)
client = await getattr(self, "client_connect")(client_sock)
self.log(f"{self.name} Accepted <lg>{client.ip}</lg>")
self._clients.append(client)
# Dispatch accept packet to client and begin client socket loop
await client.initialize()
async def on_client_disconnect(self, client):
self._clients.remove(client)
self.log(f"Client Disconnected {client.ip}")
def add_packet_handlers(self):
import inspect
members = inspect.getmembers(self)
for _, member in members:
# Register all packet handlers for inheriting server
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."""
return await self._ready.wait()
async def listen(self):
self.log(f"Listening on port <lr>{self._port}</lr>")
while self._alive.is_set():
client_sock, _ = await self._loop.sock_accept(self._serv_sock)
client_sock.setblocking(0)
client_sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
self._loop.create_task(self.on_client_accepted(client_sock))
@property
def data(self):
return self._parent.data
@property
def dispatcher(self):
return self._dispatcher
@property
def name(self):
return self._name
@property
def parent(self):
return self._parent
@property
def port(self):
return self._port
@property
def population(self):
return len(self._clients)
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 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"""
def __init__(self, parent, port):
self._loop = get_event_loop()
self._parent = parent
self._port = port
self._is_alive = False
self._clients = []
self._packet_handlers = []
self._ready = Event()
self._alive = Event()
self._dispatcher = Dispatcher(self)
self._serv_sock = socket(AF_INET, SOCK_STREAM)
self._serv_sock.setblocking(0)
self._serv_sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
self._serv_sock.bind(("127.0.0.1", self._port))
self._serv_sock.listen(0)
self.add_packet_handlers()
def log(self, message, level=None):
level = level or "info"
getattr(log, level)(f"{self._name} {message}")
@property
def alive(self):
return self._alive.is_set()
async def start(self):
self._is_alive = True
self._alive.set()
self._ready.set()
self._listener = self._loop.create_task(self.listen())
def close(self):
self._listener.cancel()
async def on_client_accepted(self, socket):
client_sock = ClientSocket(socket)
client = await getattr(self, "client_connect")(client_sock)
self.log(f"{self.name} Accepted <lg>{client.ip}</lg>")
self._clients.append(client)
# Dispatch accept packet to client and begin client socket loop
await client.initialize()
async def on_client_disconnect(self, client):
self._clients.remove(client)
self.log(f"Client Disconnected {client.ip}")
def add_packet_handlers(self):
import inspect
members = inspect.getmembers(self)
for _, member in members:
# Register all packet handlers for inheriting server
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."""
return await self._ready.wait()
async def listen(self):
self.log(f"Listening on port <lr>{self._port}</lr>")
while self._alive.is_set():
client_sock, _ = await self._loop.sock_accept(self._serv_sock)
client_sock.setblocking(0)
client_sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
self._loop.create_task(self.on_client_accepted(client_sock))
@property
def data(self):
return self._parent.data
@property
def dispatcher(self):
return self._dispatcher
@property
def name(self):
return self._name
@property
def parent(self):
return self._parent
@property
def port(self):
return self._port
@property
def population(self):
return len(self._clients)

@ -1,23 +1,14 @@
__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
__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, Random, TagPoint)

@ -1,13 +0,0 @@
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)

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

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

0
requirements.txt Normal file