aaaaaaaa
This commit is contained in:
parent
c91aa653ce
commit
683f030d55
29
.gitignore
vendored
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
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
|
||||
|
1492
mapy/db/db_client.py
1492
mapy/db/db_client.py
File diff suppressed because it is too large
Load Diff
568
mapy/db/types.py
568
mapy/db/types.py
@ -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}")
|
||||
|
199
mapy/logger.py
199
mapy/logger.py
@ -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
|
||||
|
1664
mapy/net/opcodes.py
1664
mapy/net/opcodes.py
File diff suppressed because it is too large
Load Diff
BIN
mapy/net/packet.7z
Normal file
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
0
requirements.txt
Normal file
Loading…
Reference in New Issue
Block a user