├── .gitignore ├── LICENSE.md ├── README.md ├── helpers └── handbook.txt ├── main.py ├── manage.py ├── mymodels.py ├── pythonstory ├── __init__.py ├── channel │ ├── __init__.py │ ├── commands.py │ ├── factory.py │ ├── handlers.py │ ├── map │ │ ├── __init__.py │ │ ├── handlers.py │ │ ├── mixins.py │ │ ├── models.py │ │ └── packets.py │ ├── models.py │ ├── npc │ │ ├── __init__.py │ │ ├── handlers.py │ │ └── packets.py │ ├── packethelpers.py │ ├── packets.py │ ├── player │ │ ├── __init__.py │ │ ├── handlers.py │ │ └── packets.py │ ├── processor.py │ ├── protocol.py │ └── quest │ │ ├── __init__.py │ │ ├── handlers.py │ │ ├── models.py │ │ └── packets.py ├── common │ ├── __init__.py │ ├── bitutils.py │ ├── decoder.py │ ├── decorators.py │ ├── enums.py │ ├── factory.py │ ├── gamelogicutils.py │ ├── handlers.py │ ├── helperclasses.py │ ├── mixins.py │ ├── models.py │ ├── packetbuilder.py │ ├── packethelpers.py │ ├── packetreader.py │ ├── packets.py │ ├── processor.py │ ├── protocol.py │ ├── recvopcodes.py │ ├── sendopcodes.py │ ├── settings.py │ ├── staticmodels.py │ └── tests │ │ ├── __item__.py │ │ └── test_staticmodels.py └── world │ ├── __init__.py │ ├── factory.py │ ├── handlers.py │ ├── models.py │ ├── packethelpers.py │ ├── packets.py │ ├── processor.py │ ├── protocol.py │ └── tests │ ├── __init__.py │ └── test_packets.py ├── requirements.txt └── staticdb.sqlite /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | mysql2slite.sh 4 | pymaple.sublime-project 5 | pymaple.sublime-workspace 6 | mcdbdump.sql 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin Røed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pythonstory 2 | 3 | TODO: Write a project description 4 | ## Installation 5 | 1. Clone the repo 6 | `git clone https://github.com/martolini/pythonstory.git` 7 | 2. Create a virtual environment 8 | `virtualenv venv && source venv/bin/activate` 9 | 3. Install dependencies 10 | `pip install -r requirements.txt` 11 | 12 | ## Usage 13 | 1. Create the necessary tables 14 | `python manage.py syncdb` 15 | 2. Create an account 16 | `python manage.py createaccount testing testing` 17 | 3. Start the server 18 | `python main.py` 19 | 20 | ## Contributing 21 | 1. Fork it! 22 | 2. Create your feature branch: `git checkout -b my-new-feature` 23 | 3. Commit your changes: `git commit -am 'Add some feature'` 24 | 4. Push to the branch: `git push origin my-new-feature` 25 | 5. Submit a pull request 26 | ## History 27 | TODO: Write history 28 | ## Credits 29 | TODO: Write credits 30 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from pythonstory.world import factory as worldfactory, models as worldmodels 2 | from pythonstory.channel import factory as channelfactory 3 | from pythonstory.common import settings 4 | from twisted.internet import reactor 5 | 6 | 7 | def runserver(): 8 | worlds = [worldmodels.World(i, **w) for i, w in enumerate(settings.WORLDS)] 9 | worldfac = worldfactory.WorldFactory(worlds=worlds) 10 | for world in worlds: 11 | for c in xrange(len(world.channels)): 12 | channel = channelfactory.ChannelFactory(key=c+1, 13 | world=world, 14 | worldfac=worldfac) 15 | port = 7575 + channel.key - 1 16 | port += world.key * 100 17 | channel.port = port 18 | world.channels[c] = channel 19 | reactor.listenTCP(channel.port, channel) 20 | 21 | reactor.listenTCP(8484, worldfac) 22 | reactor.run() 23 | 24 | if __name__ == '__main__': 25 | runserver() 26 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pythonstory 3 | from peewee import IntegrityError 4 | 5 | 6 | class Manager(object): 7 | def __init__(self, *args, **kwargs): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument('func', help='What function to run', type=str) 10 | parser.add_argument('args', help='Arguments to function', nargs="*") 11 | args = parser.parse_args() 12 | try: 13 | func = getattr(self, args.func) 14 | if args.args: 15 | func(*args.args) 16 | else: 17 | func() 18 | except AttributeError as e: 19 | print e 20 | print "Don't know that function." 21 | except TypeError, e: 22 | print e 23 | 24 | def syncdb(self): 25 | db = pythonstory.common.settings.DATABASES['default'] 26 | models = pythonstory.common.models.BaseModel.__subclasses__() 27 | for model in models: 28 | print 'Creating table {} for model {}'.format( 29 | model._meta.db_table, 30 | model.__name__) 31 | try: 32 | db.create_table(model) 33 | except Exception as e: 34 | print e 35 | print '.' * 20 36 | 37 | def createadmin(self): 38 | self.createaccount('admin', 'admin') 39 | 40 | def flush(self): 41 | for model in pythonstory.common.models.BaseModel.__subclasses__(): 42 | print "Deleting all {}s".format(model.__name__) 43 | model.delete().execute() 44 | 45 | def createaccount(self, name, password): 46 | from pythonstory.world.models import Account 47 | try: 48 | Account.create(name=name, password=password) 49 | print 'Successfully created account {}'.format(name) 50 | except IntegrityError as e: 51 | print e 52 | 53 | def runserver(self): 54 | import main 55 | main.runserver() 56 | 57 | 58 | if __name__ == '__main__': 59 | m = Manager() 60 | -------------------------------------------------------------------------------- /mymodels.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | database = SqliteDatabase('staticdb.sqlite', **{}) 4 | 5 | class UnknownField(object): 6 | pass 7 | 8 | class BaseModel(Model): 9 | class Meta: 10 | database = database 11 | 12 | class BlockChatReasonData(BaseModel): 13 | message = CharField() 14 | reason = CharField() 15 | 16 | class Meta: 17 | db_table = 'block_chat_reason_data' 18 | 19 | class BlockReasonData(BaseModel): 20 | block_type = CharField() 21 | message = CharField() 22 | 23 | class Meta: 24 | db_table = 'block_reason_data' 25 | 26 | class CashCommodityData(BaseModel): 27 | expiration_days = IntegerField() 28 | flags = TextField() 29 | gender = TextField() 30 | itemid = IntegerField() 31 | price = IntegerField() 32 | priority = UnknownField() # tinyint(3) 33 | quantity = IntegerField() 34 | serial_number = PrimaryKeyField() 35 | 36 | class Meta: 37 | db_table = 'cash_commodity_data' 38 | 39 | class CashPackageData(BaseModel): 40 | id = BigIntegerField(primary_key=True) 41 | packageid = IntegerField() 42 | serial_number = IntegerField() 43 | 44 | class Meta: 45 | db_table = 'cash_package_data' 46 | 47 | class CharacterCreationData(BaseModel): 48 | character_type = TextField() 49 | gender = TextField() 50 | id = BigIntegerField(primary_key=True) 51 | object_type = TextField() 52 | objectid = IntegerField() 53 | 54 | class Meta: 55 | db_table = 'character_creation_data' 56 | 57 | class CharacterFaceData(BaseModel): 58 | faceid = PrimaryKeyField() 59 | gender = TextField() 60 | 61 | class Meta: 62 | db_table = 'character_face_data' 63 | 64 | class CharacterForbiddenNames(BaseModel): 65 | forbidden_name = CharField(primary_key=True) 66 | 67 | class Meta: 68 | db_table = 'character_forbidden_names' 69 | 70 | class CharacterHairData(BaseModel): 71 | gender = TextField() 72 | hairid = PrimaryKeyField() 73 | 74 | class Meta: 75 | db_table = 'character_hair_data' 76 | 77 | class CharacterSkinData(BaseModel): 78 | skinid = UnknownField(primary_key=True) # tinyint(3) 79 | 80 | class Meta: 81 | db_table = 'character_skin_data' 82 | 83 | class CrcInfo(BaseModel): 84 | crc_integer = IntegerField() 85 | crc_string = CharField() 86 | filename = CharField(primary_key=True) 87 | 88 | class Meta: 89 | db_table = 'crc_info' 90 | 91 | class CurseData(BaseModel): 92 | word = CharField(primary_key=True) 93 | 94 | class Meta: 95 | db_table = 'curse_data' 96 | 97 | class DropData(BaseModel): 98 | chance = IntegerField() 99 | dropperid = IntegerField(index=True) 100 | flags = TextField() 101 | id = BigIntegerField(primary_key=True) 102 | itemid = IntegerField() 103 | maximum_quantity = IntegerField() 104 | minimum_quantity = IntegerField() 105 | questid = IntegerField() 106 | 107 | class Meta: 108 | db_table = 'drop_data' 109 | 110 | class DropGlobalData(BaseModel): 111 | chance = IntegerField() 112 | continent = UnknownField() # tinyint(3) 113 | flags = TextField() 114 | id = BigIntegerField(primary_key=True) 115 | itemid = IntegerField() 116 | maximum_level = UnknownField() # tinyint(3) 117 | maximum_quantity = IntegerField() 118 | minimum_level = UnknownField() # tinyint(3) 119 | minimum_quantity = IntegerField() 120 | questid = IntegerField() 121 | 122 | class Meta: 123 | db_table = 'drop_global_data' 124 | 125 | class ItemConsumeData(BaseModel): 126 | accuracy = IntegerField() 127 | avoid = IntegerField() 128 | buff_time = IntegerField() 129 | carnival_points = IntegerField() 130 | create_item = IntegerField() 131 | cure_ailments = TextField() 132 | decrease_fatigue = IntegerField() 133 | decrease_hunger = IntegerField() 134 | defense_vs_curse = UnknownField() # tinyint(3) 135 | defense_vs_darkness = UnknownField() # tinyint(3) 136 | defense_vs_fire = UnknownField() # tinyint(3) 137 | defense_vs_ice = UnknownField() # tinyint(3) 138 | defense_vs_lightning = UnknownField() # tinyint(3) 139 | defense_vs_poison = UnknownField() # tinyint(3) 140 | defense_vs_seal = UnknownField() # tinyint(3) 141 | defense_vs_stun = UnknownField() # tinyint(3) 142 | defense_vs_weakness = UnknownField() # tinyint(3) 143 | drop_up = TextField(null=True) 144 | drop_up_item = IntegerField() 145 | drop_up_item_range = IntegerField() 146 | drop_up_map_ranges = UnknownField() # tinyint(3) 147 | effect = UnknownField() # tinyint(3) 148 | flags = TextField() 149 | hp = IntegerField() 150 | hp_percentage = IntegerField() 151 | itemid = PrimaryKeyField() 152 | jump = IntegerField() 153 | magic_attack = IntegerField() 154 | magic_defense = IntegerField() 155 | morph = IntegerField() 156 | move_to = IntegerField() 157 | mp = IntegerField() 158 | mp_percentage = IntegerField() 159 | prob = UnknownField() # tinyint(3) 160 | speed = IntegerField() 161 | weapon_attack = IntegerField() 162 | weapon_defense = IntegerField() 163 | 164 | class Meta: 165 | db_table = 'item_consume_data' 166 | 167 | class ItemData(BaseModel): 168 | experience = IntegerField() 169 | flags = TextField() 170 | inventory = TextField() 171 | itemid = PrimaryKeyField() 172 | level_for_maker = IntegerField() 173 | max_level = UnknownField() # tinyint(3) 174 | max_possession_count = UnknownField() # tinyint(3) 175 | max_slot_quantity = IntegerField() 176 | min_level = UnknownField() # tinyint(3) 177 | money = IntegerField() 178 | npc = IntegerField() 179 | price = IntegerField() 180 | state_change_item = IntegerField() 181 | 182 | class Meta: 183 | db_table = 'item_data' 184 | 185 | class ItemEquipBonusExp(BaseModel): 186 | bonus_exp = IntegerField() 187 | itemid = IntegerField() 188 | req_seconds_held = IntegerField() 189 | 190 | class Meta: 191 | db_table = 'item_equip_bonus_exp' 192 | primary_key = CompositeKey('bonus_exp', 'itemid') 193 | 194 | class ItemEquipData(BaseModel): 195 | accuracy = IntegerField() 196 | attack_speed = UnknownField() # tinyint(3) 197 | avoid = IntegerField() 198 | dexterity = IntegerField() 199 | elemental_default = UnknownField() # tinyint(3) 200 | equip_slots = TextField() 201 | flags = TextField() 202 | hands = IntegerField() 203 | heal_hp = UnknownField() # tinyint(3) 204 | hp = IntegerField() 205 | inc_fire_damage = UnknownField() # tinyint(3) 206 | inc_ice_damage = UnknownField() # tinyint(3) 207 | inc_lightning_damage = UnknownField() # tinyint(3) 208 | inc_poison_damage = UnknownField() # tinyint(3) 209 | intelligence = IntegerField() 210 | itemid = PrimaryKeyField() 211 | jump = IntegerField() 212 | knockback = IntegerField() 213 | luck = IntegerField() 214 | magic_attack = IntegerField() 215 | magic_defense = IntegerField() 216 | mp = IntegerField() 217 | recovery = IntegerField() 218 | req_dex = IntegerField() 219 | req_fame = IntegerField() 220 | req_int = IntegerField() 221 | req_job = TextField() 222 | req_luk = IntegerField() 223 | req_str = IntegerField() 224 | scroll_slots = IntegerField() 225 | specialid = IntegerField() 226 | speed = IntegerField() 227 | strength = IntegerField() 228 | taming_mob = UnknownField() # tinyint(3) 229 | traction = UnknownField() # double 230 | weapon_attack = IntegerField() 231 | weapon_defense = IntegerField() 232 | 233 | class Meta: 234 | db_table = 'item_equip_data' 235 | 236 | class ItemHalloweenData(BaseModel): 237 | itemid = PrimaryKeyField() 238 | 239 | class Meta: 240 | db_table = 'item_halloween_data' 241 | 242 | class ItemMakerData(BaseModel): 243 | inc_max_hp = IntegerField() 244 | inc_max_mp = IntegerField() 245 | inc_req_level = UnknownField() # tinyint(3) 246 | itemid = PrimaryKeyField() 247 | rand_option = UnknownField() # tinyint(3) 248 | 249 | class Meta: 250 | db_table = 'item_maker_data' 251 | 252 | class ItemMonsterCardMapRanges(BaseModel): 253 | end_map = IntegerField() 254 | itemid = IntegerField() 255 | start_map = IntegerField() 256 | 257 | class Meta: 258 | db_table = 'item_monster_card_map_ranges' 259 | primary_key = CompositeKey('itemid', 'start_map') 260 | 261 | class ItemPetData(BaseModel): 262 | default_name = CharField() 263 | evolution_item = IntegerField() 264 | flags = TextField() 265 | hunger = UnknownField() # tinyint(3) 266 | itemid = IntegerField(index=True) 267 | life = IntegerField() 268 | limited_life = IntegerField() 269 | req_level_for_evolution = UnknownField() # tinyint(3) 270 | 271 | class Meta: 272 | db_table = 'item_pet_data' 273 | 274 | class ItemPetEvolutions(BaseModel): 275 | chance = IntegerField() 276 | evolution_itemid = IntegerField() 277 | itemid = IntegerField() 278 | 279 | class Meta: 280 | db_table = 'item_pet_evolutions' 281 | primary_key = CompositeKey('evolution_itemid', 'itemid') 282 | 283 | class ItemPetInteractions(BaseModel): 284 | closeness = UnknownField() # tinyint(3) 285 | command = IntegerField() 286 | itemid = IntegerField() 287 | success = UnknownField() # tinyint(3) 288 | 289 | class Meta: 290 | db_table = 'item_pet_interactions' 291 | primary_key = CompositeKey('command', 'itemid') 292 | 293 | class ItemRandomMorphs(BaseModel): 294 | id = BigIntegerField(primary_key=True) 295 | itemid = IntegerField() 296 | morphid = UnknownField() # tinyint(3) 297 | success = UnknownField() # tinyint(3) 298 | 299 | class Meta: 300 | db_table = 'item_random_morphs' 301 | 302 | class ItemRechargeableData(BaseModel): 303 | itemid = PrimaryKeyField() 304 | unit_price = UnknownField() # double(2,1) 305 | weapon_attack = IntegerField() 306 | 307 | class Meta: 308 | db_table = 'item_rechargeable_data' 309 | 310 | class ItemRewardData(BaseModel): 311 | effect = CharField(null=True) 312 | id = BigIntegerField(primary_key=True) 313 | itemid = IntegerField() 314 | prob = IntegerField() 315 | quantity = IntegerField() 316 | rewardid = IntegerField() 317 | 318 | class Meta: 319 | db_table = 'item_reward_data' 320 | 321 | class ItemScrollData(BaseModel): 322 | break_item = IntegerField() 323 | flags = TextField() 324 | iacc = IntegerField() 325 | iavo = IntegerField() 326 | idex = IntegerField() 327 | ihp = IntegerField() 328 | iint = IntegerField() 329 | ijump = IntegerField() 330 | iluk = IntegerField() 331 | imatk = IntegerField() 332 | imdef = IntegerField() 333 | imp = IntegerField() 334 | ispeed = IntegerField() 335 | istr = IntegerField() 336 | itemid = PrimaryKeyField() 337 | iwatk = IntegerField() 338 | iwdef = IntegerField() 339 | success = IntegerField() 340 | 341 | class Meta: 342 | db_table = 'item_scroll_data' 343 | 344 | class ItemScrollTargets(BaseModel): 345 | id = BigIntegerField(primary_key=True) 346 | req_itemid = IntegerField() 347 | scrollid = IntegerField() 348 | 349 | class Meta: 350 | db_table = 'item_scroll_targets' 351 | 352 | class ItemSkills(BaseModel): 353 | chance = UnknownField() # tinyint(3) 354 | itemid = IntegerField() 355 | master_level = UnknownField() # tinyint(3) 356 | req_skill_level = UnknownField() # tinyint(3) 357 | skillid = IntegerField() 358 | 359 | class Meta: 360 | db_table = 'item_skills' 361 | primary_key = CompositeKey('itemid', 'master_level', 'skillid') 362 | 363 | class ItemSummons(BaseModel): 364 | chance = IntegerField() 365 | id = BigIntegerField(primary_key=True) 366 | itemid = IntegerField(index=True) 367 | mobid = IntegerField() 368 | 369 | class Meta: 370 | db_table = 'item_summons' 371 | 372 | class ItemTimelessLevels(BaseModel): 373 | accuracy_max = IntegerField() 374 | accuracy_min = IntegerField() 375 | avoidability_max = IntegerField() 376 | avoidability_min = IntegerField() 377 | dex_max = IntegerField() 378 | dex_min = IntegerField() 379 | experience = IntegerField() 380 | hp_max = IntegerField() 381 | hp_min = IntegerField() 382 | int_max = IntegerField() 383 | int_min = IntegerField() 384 | item_level = UnknownField() # tinyint(3) 385 | itemid = IntegerField() 386 | jump_max = IntegerField() 387 | jump_min = IntegerField() 388 | luk_max = IntegerField() 389 | luk_min = IntegerField() 390 | magic_attack_max = IntegerField() 391 | magic_attack_min = IntegerField() 392 | magic_defense_max = IntegerField() 393 | magic_defense_min = IntegerField() 394 | mp_max = IntegerField() 395 | mp_min = IntegerField() 396 | speed_max = IntegerField() 397 | speed_min = IntegerField() 398 | str_max = IntegerField() 399 | str_min = IntegerField() 400 | weapon_attack_max = IntegerField() 401 | weapon_attack_min = IntegerField() 402 | weapon_defense_max = IntegerField() 403 | weapon_defense_min = IntegerField() 404 | 405 | class Meta: 406 | db_table = 'item_timeless_levels' 407 | primary_key = CompositeKey('item_level', 'itemid') 408 | 409 | class ItemTimelessSkills(BaseModel): 410 | item_level = UnknownField() # tinyint(3) 411 | itemid = IntegerField() 412 | probability = UnknownField() # tinyint(3) 413 | skill_level = UnknownField() # tinyint(3) 414 | skillid = IntegerField() 415 | 416 | class Meta: 417 | db_table = 'item_timeless_skills' 418 | primary_key = CompositeKey('itemid', 'skillid') 419 | 420 | class MakerCreationData(BaseModel): 421 | catalyst = IntegerField() 422 | classid = TextField() 423 | itemid = IntegerField() 424 | quantity = IntegerField() 425 | req_equip = IntegerField() 426 | req_item = IntegerField() 427 | req_level = UnknownField() # tinyint(3) 428 | req_maker_level = UnknownField() # tinyint(3) 429 | req_money = IntegerField() 430 | upgrade_crystals = UnknownField() # tinyint(3) 431 | 432 | class Meta: 433 | db_table = 'maker_creation_data' 434 | primary_key = CompositeKey('classid', 'itemid') 435 | 436 | class MakerRecipes(BaseModel): 437 | itemid = IntegerField() 438 | quantity = IntegerField() 439 | req_item = IntegerField() 440 | 441 | class Meta: 442 | db_table = 'maker_recipes' 443 | primary_key = CompositeKey('itemid', 'req_item') 444 | 445 | class MakerRewards(BaseModel): 446 | chance = UnknownField() # tinyint(3) 447 | itemid = IntegerField() 448 | quantity = IntegerField() 449 | rewardid = IntegerField() 450 | 451 | class Meta: 452 | db_table = 'maker_rewards' 453 | primary_key = CompositeKey('itemid', 'rewardid') 454 | 455 | class MapContinentData(BaseModel): 456 | continent = UnknownField() # tinyint(3) 457 | map_cluster = UnknownField() # tinyint(3) 458 | 459 | class Meta: 460 | db_table = 'map_continent_data' 461 | primary_key = CompositeKey('continent', 'map_cluster') 462 | 463 | class MapData(BaseModel): 464 | damage_per_second = IntegerField() 465 | decrease_hp = UnknownField() # tinyint(3) 466 | default_bgm = CharField() 467 | default_traction = UnknownField() # double(20,15) 468 | field_limitations = TextField() 469 | field_type = TextField() 470 | flags = TextField() 471 | forced_return_map = IntegerField() 472 | link = IntegerField() 473 | map_ltx = IntegerField() 474 | map_lty = IntegerField() 475 | map_rbx = IntegerField() 476 | map_rby = IntegerField() 477 | mapid = PrimaryKeyField() 478 | min_level_limit = UnknownField() # tinyint(3) 479 | mob_rate = UnknownField() # double(13,11) 480 | protect_item = IntegerField() 481 | regen_rate = UnknownField() # tinyint(3) 482 | return_map = IntegerField() 483 | ship_kind = UnknownField() # tinyint(3) 484 | shuffle_name = CharField() 485 | time_limit = IntegerField() 486 | 487 | class Meta: 488 | db_table = 'map_data' 489 | 490 | class MapFootholds(BaseModel): 491 | drag_force = IntegerField() 492 | flags = TextField() 493 | id = IntegerField() 494 | mapid = IntegerField() 495 | nextid = IntegerField() 496 | previousid = IntegerField() 497 | x1 = IntegerField() 498 | x2 = IntegerField() 499 | y1 = IntegerField() 500 | y2 = IntegerField() 501 | 502 | class Meta: 503 | db_table = 'map_footholds' 504 | primary_key = CompositeKey('id', 'mapid') 505 | 506 | class MapLife(BaseModel): 507 | flags = TextField() 508 | foothold = IntegerField() 509 | id = BigIntegerField(primary_key=True) 510 | life_name = CharField(null=True) 511 | life_type = TextField() 512 | lifeid = IntegerField() 513 | mapid = IntegerField() 514 | max_click_pos = IntegerField() 515 | min_click_pos = IntegerField() 516 | respawn_time = IntegerField() 517 | x_pos = IntegerField() 518 | y_pos = IntegerField() 519 | 520 | class Meta: 521 | db_table = 'map_life' 522 | 523 | class MapPortals(BaseModel): 524 | destination = IntegerField() 525 | destination_label = CharField(null=True) 526 | flags = TextField() 527 | id = IntegerField() 528 | label = CharField(null=True) 529 | mapid = IntegerField() 530 | script = CharField(null=True) 531 | x_pos = IntegerField() 532 | y_pos = IntegerField() 533 | 534 | class Meta: 535 | db_table = 'map_portals' 536 | primary_key = CompositeKey('id', 'mapid') 537 | 538 | class MapSeats(BaseModel): 539 | mapid = IntegerField() 540 | seatid = IntegerField() 541 | x_pos = IntegerField() 542 | y_pos = IntegerField() 543 | 544 | class Meta: 545 | db_table = 'map_seats' 546 | primary_key = CompositeKey('mapid', 'seatid') 547 | 548 | class MapTimeMob(BaseModel): 549 | end_hour = UnknownField() # tinyint(3) 550 | mapid = PrimaryKeyField() 551 | message = CharField() 552 | mobid = IntegerField() 553 | start_hour = UnknownField() # tinyint(3) 554 | 555 | class Meta: 556 | db_table = 'map_time_mob' 557 | 558 | class McdbInfo(BaseModel): 559 | maple_locale = TextField() 560 | maple_version = IntegerField() 561 | subversion = IntegerField() 562 | test_server = UnknownField() # tinyint(1) 563 | version = IntegerField() 564 | 565 | class Meta: 566 | db_table = 'mcdb_info' 567 | 568 | class MobAttacks(BaseModel): 569 | attack_type = TextField(null=True) 570 | attackid = IntegerField() 571 | element = TextField(null=True) 572 | flags = TextField() 573 | mob_skill_level = IntegerField() 574 | mob_skillid = IntegerField() 575 | mobid = IntegerField() 576 | mp_burn = IntegerField() 577 | mp_cost = IntegerField() 578 | 579 | class Meta: 580 | db_table = 'mob_attacks' 581 | primary_key = CompositeKey('attackid', 'mobid') 582 | 583 | class MobData(BaseModel): 584 | accuracy = IntegerField() 585 | avoidability = IntegerField() 586 | carnival_points = UnknownField() # tinyint(3) 587 | chase_speed = IntegerField() 588 | damaged_by_mob_only = IntegerField() 589 | damaged_by_skill_only = IntegerField() 590 | death_after = IntegerField() 591 | death_buff = IntegerField() 592 | drop_item_period = IntegerField() 593 | experience = IntegerField() 594 | explode_hp = IntegerField() 595 | fire_modifier = TextField() 596 | fixed_damage = IntegerField() 597 | flags = TextField() 598 | holy_modifier = TextField() 599 | hp = IntegerField() 600 | hp_bar_bg_color = UnknownField() # tinyint(3) 601 | hp_bar_color = UnknownField() # tinyint(3) 602 | hp_recovery = IntegerField() 603 | ice_modifier = TextField() 604 | knockback = IntegerField() 605 | lightning_modifier = TextField() 606 | link = IntegerField() 607 | magical_attack = IntegerField() 608 | magical_defense = IntegerField() 609 | mob_level = IntegerField() 610 | mobid = PrimaryKeyField() 611 | mp = IntegerField() 612 | mp_recovery = IntegerField() 613 | nonelemental_modifier = TextField() 614 | physical_attack = IntegerField() 615 | physical_defense = IntegerField() 616 | poison_modifier = TextField() 617 | speed = IntegerField() 618 | summon_type = IntegerField() 619 | traction = UnknownField() # double(3,1) 620 | 621 | class Meta: 622 | db_table = 'mob_data' 623 | 624 | class MobSkills(BaseModel): 625 | effect_delay = IntegerField() 626 | id = BigIntegerField(primary_key=True) 627 | mobid = IntegerField() 628 | skill_level = IntegerField() 629 | skillid = IntegerField() 630 | 631 | class Meta: 632 | db_table = 'mob_skills' 633 | 634 | class MobSummons(BaseModel): 635 | id = BigIntegerField(primary_key=True) 636 | mobid = IntegerField(index=True) 637 | summonid = IntegerField() 638 | 639 | class Meta: 640 | db_table = 'mob_summons' 641 | 642 | class MonsterCardData(BaseModel): 643 | cardid = PrimaryKeyField() 644 | mobid = IntegerField() 645 | 646 | class Meta: 647 | db_table = 'monster_card_data' 648 | 649 | class MorphData(BaseModel): 650 | flags = TextField() 651 | jump = IntegerField() 652 | morphid = PrimaryKeyField() 653 | speed = IntegerField() 654 | swim = UnknownField() # double(4,1) 655 | traction = UnknownField() # double(4,1) 656 | 657 | class Meta: 658 | db_table = 'morph_data' 659 | 660 | class NpcData(BaseModel): 661 | flags = TextField() 662 | npcid = PrimaryKeyField() 663 | storage_cost = IntegerField() 664 | 665 | class Meta: 666 | db_table = 'npc_data' 667 | 668 | class OxQuizData(BaseModel): 669 | answer = TextField() 670 | display = CharField() 671 | question = CharField() 672 | question_set = IntegerField() 673 | questionid = IntegerField() 674 | 675 | class Meta: 676 | db_table = 'ox_quiz_data' 677 | primary_key = CompositeKey('question_set', 'questionid') 678 | 679 | class PhysicalDefenseData(BaseModel): 680 | class_ = UnknownField(db_column='class') # tinyint(3) 681 | defense = IntegerField() 682 | player_level = UnknownField() # tinyint(3) 683 | 684 | class Meta: 685 | db_table = 'physical_defense_data' 686 | primary_key = CompositeKey('class_', 'player_level') 687 | 688 | class QuestAreaData(BaseModel): 689 | area_name = CharField() 690 | areaid = UnknownField(primary_key=True) # tinyint(3) 691 | 692 | class Meta: 693 | db_table = 'quest_area_data' 694 | 695 | class QuestData(BaseModel): 696 | fame = IntegerField() 697 | flags = TextField() 698 | max_level = IntegerField() 699 | min_level = IntegerField() 700 | next_quest = IntegerField() 701 | pet_closeness = IntegerField() 702 | quest_area = IntegerField() 703 | questid = PrimaryKeyField() 704 | repeat_wait = IntegerField() 705 | taming_mob_level = IntegerField() 706 | time_limit = IntegerField() 707 | 708 | class Meta: 709 | db_table = 'quest_data' 710 | 711 | class QuestExclusiveMedals(BaseModel): 712 | questid = PrimaryKeyField() 713 | 714 | class Meta: 715 | db_table = 'quest_exclusive_medals' 716 | 717 | class QuestRequests(BaseModel): 718 | objectid = IntegerField() 719 | quantity = IntegerField() 720 | quest_state = TextField() 721 | questid = IntegerField(index=True) 722 | request_type = TextField() 723 | 724 | class Meta: 725 | db_table = 'quest_requests' 726 | primary_key = CompositeKey('objectid', 'quest_state', 'questid') 727 | 728 | class QuestRequiredJobs(BaseModel): 729 | questid = IntegerField() 730 | valid_jobid = IntegerField() 731 | 732 | class Meta: 733 | db_table = 'quest_required_jobs' 734 | primary_key = CompositeKey('questid', 'valid_jobid') 735 | 736 | class QuestRewards(BaseModel): 737 | flags = TextField() 738 | gender = TextField() 739 | id = BigIntegerField(primary_key=True) 740 | job = IntegerField() 741 | job_tracks = TextField() 742 | master_level = IntegerField() 743 | prop = IntegerField() 744 | quantity = IntegerField() 745 | quest_state = TextField() 746 | questid = IntegerField(index=True) 747 | reward_type = TextField() 748 | rewardid = IntegerField() 749 | 750 | class Meta: 751 | db_table = 'quest_rewards' 752 | 753 | class ReactorData(BaseModel): 754 | flags = TextField() 755 | link = IntegerField() 756 | max_states = UnknownField() # tinyint(3) 757 | reactorid = PrimaryKeyField() 758 | 759 | class Meta: 760 | db_table = 'reactor_data' 761 | 762 | class ReactorEventTriggerSkills(BaseModel): 763 | reactorid = IntegerField() 764 | skillid = IntegerField() 765 | state = UnknownField() # tinyint(3) 766 | 767 | class Meta: 768 | db_table = 'reactor_event_trigger_skills' 769 | 770 | class ReactorEvents(BaseModel): 771 | event_type = TextField() 772 | itemid = IntegerField() 773 | ltx = IntegerField() 774 | lty = IntegerField() 775 | next_state = UnknownField() # tinyint(3) 776 | quantity = IntegerField() 777 | rbx = IntegerField() 778 | rby = IntegerField() 779 | reactorid = IntegerField(index=True) 780 | state = UnknownField() # tinyint(3) 781 | timeout = IntegerField() 782 | 783 | class Meta: 784 | db_table = 'reactor_events' 785 | primary_key = CompositeKey('next_state', 'reactorid', 'state') 786 | 787 | class Scripts(BaseModel): 788 | helper = UnknownField() # tinyint(3) 789 | objectid = IntegerField() 790 | script = CharField() 791 | script_type = TextField() 792 | 793 | class Meta: 794 | db_table = 'scripts' 795 | primary_key = CompositeKey('helper', 'objectid', 'script_type') 796 | 797 | class ShopData(BaseModel): 798 | npcid = IntegerField() 799 | recharge_tier = IntegerField() 800 | shopid = PrimaryKeyField() 801 | 802 | class Meta: 803 | db_table = 'shop_data' 804 | 805 | class ShopItems(BaseModel): 806 | itemid = IntegerField() 807 | price = IntegerField() 808 | quantity = IntegerField() 809 | shopid = IntegerField() 810 | sort = IntegerField() 811 | 812 | class Meta: 813 | db_table = 'shop_items' 814 | primary_key = CompositeKey('shopid', 'sort') 815 | 816 | class ShopRechargeData(BaseModel): 817 | itemid = IntegerField() 818 | price = UnknownField() # double(2,1) 819 | tierid = IntegerField() 820 | 821 | class Meta: 822 | db_table = 'shop_recharge_data' 823 | primary_key = CompositeKey('itemid', 'tierid') 824 | 825 | class SkillFamilyData(BaseModel): 826 | amount = IntegerField() 827 | buff_time = IntegerField() 828 | description = CharField() 829 | rep_cost = IntegerField() 830 | skill_type = TextField() 831 | skillid = PrimaryKeyField() 832 | target = TextField() 833 | title = CharField() 834 | 835 | class Meta: 836 | db_table = 'skill_family_data' 837 | 838 | class SkillMobBanishData(BaseModel): 839 | destination = IntegerField() 840 | message = CharField() 841 | mobid = PrimaryKeyField() 842 | portal = CharField() 843 | 844 | class Meta: 845 | db_table = 'skill_mob_banish_data' 846 | 847 | class SkillMobData(BaseModel): 848 | buff_time = IntegerField() 849 | chance = IntegerField() 850 | cooldown = IntegerField() 851 | hp_limit_percentage = IntegerField() 852 | ltx = IntegerField() 853 | lty = IntegerField() 854 | mp_cost = IntegerField() 855 | rbx = IntegerField() 856 | rby = IntegerField() 857 | skill_level = IntegerField() 858 | skillid = IntegerField() 859 | summon_effect = IntegerField() 860 | summon_limit = IntegerField() 861 | target_count = IntegerField() 862 | x_property = IntegerField() 863 | y_property = IntegerField() 864 | 865 | class Meta: 866 | db_table = 'skill_mob_data' 867 | 868 | class SkillMobSummons(BaseModel): 869 | level = IntegerField(index=True) 870 | mobid = IntegerField() 871 | 872 | class Meta: 873 | db_table = 'skill_mob_summons' 874 | 875 | class SkillMonsterCarnival(BaseModel): 876 | buff_time = IntegerField() 877 | buff_type = TextField() 878 | carnival_point_cost = IntegerField() 879 | mob_skill_level = UnknownField() # tinyint(3) 880 | mob_skillid = UnknownField() # tinyint(3) 881 | mobid = IntegerField() 882 | prop = UnknownField() # tinyint(3) 883 | skill_type = TextField() 884 | skillid = UnknownField() # tinyint(3) 885 | target_type = TextField() 886 | 887 | class Meta: 888 | db_table = 'skill_monster_carnival' 889 | primary_key = CompositeKey('skill_type', 'skillid') 890 | 891 | class SkillPlayerData(BaseModel): 892 | flags = TextField() 893 | skill_type = TextField() 894 | skillid = PrimaryKeyField() 895 | weapon = IntegerField() 896 | 897 | class Meta: 898 | db_table = 'skill_player_data' 899 | 900 | class SkillPlayerLevelData(BaseModel): 901 | accuracy = IntegerField() 902 | avoid = IntegerField() 903 | buff_time = IntegerField() 904 | bullet_cost = IntegerField() 905 | cooldown_time = IntegerField() 906 | critical_damage = UnknownField() # tinyint(3) 907 | damage = IntegerField() 908 | fixed_damage = IntegerField() 909 | hit_count = UnknownField() # tinyint(3) 910 | hp = IntegerField() 911 | hp_cost = IntegerField() 912 | item_cost = IntegerField() 913 | item_count = IntegerField() 914 | jump = IntegerField() 915 | ltx = IntegerField() 916 | lty = IntegerField() 917 | magic_atk = IntegerField() 918 | magic_def = IntegerField() 919 | mastery = UnknownField() # tinyint(3) 920 | mob_count = UnknownField() # tinyint(3) 921 | money_cost = IntegerField() 922 | morph = IntegerField() 923 | mp = IntegerField() 924 | mp_cost = IntegerField() 925 | optional_item_cost = IntegerField() 926 | prop = IntegerField() 927 | range = IntegerField() 928 | rbx = IntegerField() 929 | rby = IntegerField() 930 | skill_level = IntegerField() 931 | skillid = IntegerField() 932 | speed = IntegerField() 933 | str = IntegerField() 934 | weapon_atk = IntegerField() 935 | weapon_def = IntegerField() 936 | x_property = IntegerField() 937 | y_property = IntegerField() 938 | 939 | class Meta: 940 | db_table = 'skill_player_level_data' 941 | primary_key = CompositeKey('skill_level', 'skillid') 942 | 943 | class SkillPlayerRequirementData(BaseModel): 944 | req_level = UnknownField() # tinyint(3) 945 | req_skillid = IntegerField() 946 | skillid = IntegerField() 947 | 948 | class Meta: 949 | db_table = 'skill_player_requirement_data' 950 | primary_key = CompositeKey('req_skillid', 'skillid') 951 | 952 | class Strings(BaseModel): 953 | label = CharField(index=True) 954 | object_type = TextField() 955 | objectid = IntegerField() 956 | 957 | class Meta: 958 | db_table = 'strings' 959 | primary_key = CompositeKey('object_type', 'objectid') 960 | 961 | class TamingMobData(BaseModel): 962 | fatigue = UnknownField() # tinyint(3) 963 | jump = IntegerField() 964 | speed = IntegerField() 965 | swim = UnknownField() # double(4,1) 966 | tamingmobid = UnknownField(primary_key=True) # tinyint(3) 967 | traction = UnknownField() # double(4,1) 968 | 969 | class Meta: 970 | db_table = 'taming_mob_data' 971 | 972 | class UserDropData(BaseModel): 973 | chance = IntegerField() 974 | dropperid = IntegerField(index=True) 975 | flags = TextField() 976 | id = BigIntegerField(primary_key=True) 977 | itemid = IntegerField() 978 | maximum_quantity = IntegerField() 979 | minimum_quantity = IntegerField() 980 | questid = IntegerField() 981 | 982 | class Meta: 983 | db_table = 'user_drop_data' 984 | 985 | class UserShopData(BaseModel): 986 | npcid = IntegerField() 987 | recharge_tier = IntegerField() 988 | shopid = PrimaryKeyField() 989 | 990 | class Meta: 991 | db_table = 'user_shop_data' 992 | 993 | class UserShopItems(BaseModel): 994 | itemid = IntegerField() 995 | price = IntegerField() 996 | quantity = IntegerField() 997 | shopid = IntegerField() 998 | sort = IntegerField() 999 | 1000 | class Meta: 1001 | db_table = 'user_shop_items' 1002 | primary_key = CompositeKey('shopid', 'sort') 1003 | 1004 | -------------------------------------------------------------------------------- /pythonstory/__init__.py: -------------------------------------------------------------------------------- 1 | import channel 2 | import common 3 | import world 4 | -------------------------------------------------------------------------------- /pythonstory/channel/__init__.py: -------------------------------------------------------------------------------- 1 | import models 2 | import quest.models 3 | -------------------------------------------------------------------------------- /pythonstory/channel/commands.py: -------------------------------------------------------------------------------- 1 | def gomap(client, mapid): 2 | client.change_map(int(mapid)) 3 | -------------------------------------------------------------------------------- /pythonstory/channel/factory.py: -------------------------------------------------------------------------------- 1 | from ..common.factory import MapleFactory 2 | from .protocol import ChannelProtocol 3 | 4 | 5 | class ChannelFactory(MapleFactory): 6 | protocol = ChannelProtocol 7 | 8 | def __init__(self, key, world, worldfac, *args, **kwargs): 9 | self.key = key 10 | self.world = world 11 | self.world_factory = worldfac 12 | super(ChannelFactory, self).__init__(*args, **kwargs) 13 | -------------------------------------------------------------------------------- /pythonstory/channel/handlers.py: -------------------------------------------------------------------------------- 1 | from . import packets as channelpackets 2 | from .map import models as mapmodels 3 | 4 | 5 | def load_player(packet, client): 6 | char_id = packet.read_int() 7 | character = client.factory.world_factory.get_character(char_id) 8 | 9 | character.channel = client.factory.key 10 | client.character = character 11 | 12 | # TODO BUFFS 13 | # TODO DUEY 14 | client.send(channelpackets.char_info(character, client.factory.key)) 15 | client.send(channelpackets.keymap(character)) 16 | # client.send(channelpackets.skillmacros(character)) NOT IMPLEMENTED 17 | # client.send(channelpackets.buddy_list(character)) NOT IMPLEMENTED 18 | # client.send(channelpackets.load_family(character)) NOT IMPLEMENTED 19 | # TODO NOTES 20 | # TODO PARTY 21 | mapmodels.Map.get(character.map, client.factory.key).add_client(client) 22 | -------------------------------------------------------------------------------- /pythonstory/channel/map/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martolini/pythonstory/121701004fd7f2e326c4969e8400958d39c74f76/pythonstory/channel/map/__init__.py -------------------------------------------------------------------------------- /pythonstory/channel/map/handlers.py: -------------------------------------------------------------------------------- 1 | def parse_movement(packet): 2 | foothold = 0 3 | stance = 0 4 | x = 0 5 | y = 0 6 | n_commands = packet.read_byte() 7 | for i in xrange(n_commands): 8 | mtype = packet.read_byte() 9 | if mtype in (0, 15, 17): 10 | x = packet.read_short() 11 | y = packet.read_short() 12 | packet.read_int() 13 | foothold = packet.read_short() 14 | stance = packet.read_byte() 15 | packet.read_short() 16 | elif mtype in (1, 2, 6, 12, 13, 16): 17 | x = packet.read_short() 18 | y = packet.read_short() 19 | stance = packet.read_byte() 20 | foothold = packet.read_short() 21 | elif mtype in (3, 4, 7, 8, 9, 14): 22 | packet.skip(9) 23 | elif mtype == 10: 24 | packet.read_byte() 25 | print 'Should change equip, wtf' 26 | elif mtype == 11: 27 | x = packet.read_short() 28 | y = packet.read_short() 29 | foothold = packet.read_short() 30 | stance = packet.read_byte() 31 | packet.read_short() 32 | elif mtype == 15: 33 | x = packet.read_short() 34 | y = packet.read_short() 35 | packet.read_int() 36 | packet.read_short() 37 | foothold = packet.read_short() 38 | stance = packet.read_byte() 39 | packet.read_short() 40 | elif mtype == 21: 41 | packet.skip(3) 42 | else: 43 | print 'Unknown movement type {}'.format(mtype) 44 | break 45 | 46 | return x, y, foothold, stance 47 | -------------------------------------------------------------------------------- /pythonstory/channel/map/mixins.py: -------------------------------------------------------------------------------- 1 | class MovableMixin(object): 2 | def __init__(self, *args, **kwargs): 3 | self._xpos = 0 4 | self._ypos = 0 5 | self._foothold = 0 6 | self._stance = 0 7 | super(MovableMixin, self).__init__(*args, **kwargs) 8 | 9 | def move(self, x, y, foothold, stance): 10 | self._xpos = x 11 | self._ypos = y 12 | self._foothold = foothold 13 | self._stance = stance 14 | -------------------------------------------------------------------------------- /pythonstory/channel/map/models.py: -------------------------------------------------------------------------------- 1 | from pythonstory.common.staticmodels import ( 2 | MapData, MapSeats, MapPortals, MapLife 3 | ) 4 | from pythonstory.common.helperclasses import Point, Rect 5 | from . import packets as mappackets 6 | import pythonstory.channel.npc.packets as npcpackets 7 | 8 | 9 | class Map(object): 10 | cache = {c: {} for c in xrange(5)} 11 | 12 | def __init__(self, mapid): 13 | self.npcid = 200 14 | self.mobid = 500 15 | self.id = mapid 16 | self.seats = {} 17 | self.clients = set() 18 | self.portals = {} 19 | self.npc = {} 20 | self.mob = {} 21 | self.reactor = {} 22 | 23 | @classmethod 24 | def get(cls, mapid, channel): 25 | if mapid in cls.cache[channel]: 26 | return cls.cache[channel][mapid] 27 | 28 | mmap = Map(mapid) 29 | mmap.load_data(MapData.get(MapData.mapid == mmap.id)) 30 | mmap.load_seats(MapSeats.select().where(MapSeats.mapid == mmap.id)) 31 | for portal in MapPortals.select( 32 | ).where(MapPortals.mapid == mmap.id): 33 | mmap.portals[portal.id] = portal 34 | mmap.load_life(MapLife.select().where(MapLife.mapid == mmap.id)) 35 | cls.cache[channel][mmap.id] = mmap 36 | return mmap 37 | 38 | def add_client(self, client): 39 | self.clients.add(client) 40 | self.show_objects(client) 41 | self.spawn_mobs(client) 42 | 43 | def remove_client(self, client): 44 | self.clients.remove(client) 45 | 46 | def get_portal_from_string(self, string): 47 | for k, v in self.portals.iteritems(): 48 | if v.label == string: 49 | return self.portals[k] 50 | raise Exception("{} does not exist in map {}".format( 51 | string, self.id)) 52 | 53 | def load_data(self, data): 54 | self.link = data.link 55 | self.return_map = data.return_map 56 | self.forced_return_map = data.forced_return_map 57 | self.spawn_rate = data.mob_rate 58 | self.music = data.default_bgm 59 | self.dimensions = Rect( 60 | Point(data.map_ltx, data.map_lty), 61 | Point(data.map_rbx, data.map_rby) 62 | ) 63 | self.shuffle_name = data.shuffle_name 64 | self.regular_hp_decrease = data.decrease_hp 65 | self.traction = data.default_traction 66 | self.regen_rate = data.regen_rate 67 | self.min_level_limit = data.min_level_limit 68 | self.time_limit = data.time_limit 69 | self.protect_item = data.protect_item 70 | self.damage_per_second = data.damage_per_second 71 | self.ship_kind = data.ship_kind 72 | 73 | def load_seats(self, seats): 74 | for seat in seats: 75 | self.seats[seat.seatid] = Point(seat.x_pos, seat.y_pos) 76 | 77 | def load_life(self, data): 78 | for life in data: 79 | storage = None 80 | if life.life_type == 'mob': 81 | storage = self.mob 82 | life.id = self.mobid 83 | self.mobid += 1 84 | elif life.life_type == 'npc': 85 | self.npcid += 1 86 | storage = self.npc 87 | life.id = self.npcid 88 | if storage is not None: 89 | storage[life.id] = life 90 | 91 | def send(self, packet): 92 | for client in self.clients: 93 | client.send(packet) 94 | 95 | def spawn_mobs(self, client): 96 | for mob in self.mob.values(): 97 | client.send(mappackets.spawn_mob(mob)) 98 | client.send(mappackets.control_mob(mob)) 99 | 100 | def show_objects(self, client): 101 | for npc in self.npc.values(): 102 | client.send(npcpackets.spawn_npc(npc)) 103 | client.send(npcpackets.control_npc(npc)) 104 | -------------------------------------------------------------------------------- /pythonstory/channel/map/packets.py: -------------------------------------------------------------------------------- 1 | from pythonstory.common.decorators import packet 2 | from pythonstory.common import sendopcodes 3 | 4 | 5 | @packet(sendopcodes.SPAWN_MONSTER) 6 | def spawn_mob(builder, monster): 7 | (builder 8 | .write_int(monster.id) 9 | .write(5) # CONTROL 10 | .write_int(monster.lifeid) 11 | .skip(15) 12 | .write(0x88) 13 | .skip(6) 14 | .write_short(monster.x_pos) # MONSTER X POS 15 | .write_short(monster.y_pos) # MONSTER Y POS 16 | .write(5) # MOB STANCE 17 | .write_short(0) 18 | .write_short(monster.foothold) 19 | .write(-2) # NEWSPAWN -2, else -1 20 | .write(0) # GET TEAM 21 | .write_int(0) 22 | ) 23 | return builder.get_packet() 24 | 25 | 26 | @packet(sendopcodes.SPAWN_MONSTER_CONTROL) 27 | def control_mob(builder, monster): 28 | (builder 29 | .write(1) 30 | .write_int(monster.id) 31 | .write(5) # CONTROL 32 | .write_int(monster.lifeid) 33 | .skip(15) 34 | .write(0x88) 35 | .skip(6) 36 | .write_short(monster.x_pos) # MONSTER X POS 37 | .write_short(monster.y_pos) # MONSTER Y POS 38 | .write(5) # MOB STANCE 39 | .write_short(0) 40 | .write_short(monster.foothold) 41 | .write(-2) # NEWSPAWN -2, else -1 42 | .write(0) # GET TEAM 43 | .write_int(0) 44 | ) 45 | return builder.get_packet() 46 | -------------------------------------------------------------------------------- /pythonstory/channel/models.py: -------------------------------------------------------------------------------- 1 | from peewee import ForeignKeyField, IntegerField,\ 2 | DateTimeField, BooleanField, CharField,\ 3 | BigIntegerField 4 | from datetime import datetime 5 | import re 6 | from ..common.models import BaseModel 7 | from ..world.models import Account 8 | from ..common import enums, gamelogicutils, staticmodels, settings 9 | from .map.mixins import MovableMixin 10 | 11 | 12 | class Character(MovableMixin, BaseModel): 13 | account = ForeignKeyField(Account, related_name='characters') 14 | world = IntegerField(default=0) 15 | name = CharField(max_length=11) 16 | level = IntegerField(default=1) 17 | exp = IntegerField(default=0) 18 | gachaexp = IntegerField(default=0) 19 | str = IntegerField(default=12) 20 | dex = IntegerField(default=5) 21 | luk = IntegerField(default=4) 22 | int = IntegerField(default=4) 23 | hp = IntegerField(default=50) 24 | mp = IntegerField(default=5) 25 | maxhp = IntegerField(default=50) 26 | maxmp = IntegerField(default=5) 27 | meso = IntegerField(default=0) 28 | hp_mp_used = IntegerField(default=0) 29 | job = IntegerField(default=0) 30 | skin_color = IntegerField(default=0) 31 | gender = IntegerField(default=0) 32 | fame = IntegerField(default=0) 33 | hair = IntegerField(default=0) 34 | face = IntegerField(default=0) 35 | ap = IntegerField(default=0) 36 | sp = IntegerField(default=0) 37 | map = IntegerField(default=0) 38 | spawn_point = IntegerField(default=0) 39 | gm = IntegerField(default=0) 40 | party = IntegerField(default=0) 41 | buddy_capacity = IntegerField(default=25) 42 | created_at = DateTimeField(default=datetime.now()) 43 | rank = IntegerField(default=1) 44 | rank_move = IntegerField(default=0) 45 | job_rank = IntegerField(default=1) 46 | job_rank_move = IntegerField(default=0) 47 | # Missing guild/rank 48 | # Missing messenger 49 | # missing mount 50 | # missing omok 51 | # missing matchcard 52 | merchant_mesos = IntegerField(default=0) 53 | has_merchant = BooleanField(default=0) 54 | equip_slots = IntegerField(default=24) 55 | use_slots = IntegerField(default=24) 56 | setup_slots = IntegerField(default=24) 57 | etc_slots = IntegerField(default=24) 58 | # no family 59 | # missing monsterbook 60 | # missing alliance 61 | # missing vanquisher 62 | dojo_points = IntegerField(default=0) 63 | last_dojo_stage = IntegerField(default=0) 64 | finished_dojo_tutorial = BooleanField(default=False) 65 | 66 | # Ingame stuff, non DB related 67 | 68 | @classmethod 69 | def name_is_available(cls, name): 70 | if not re.match(r'[a-zA-Z0-9_-]{3,12}', name): 71 | return False 72 | if 'gm' in name: 73 | return False 74 | return cls.select().where(cls.name == name).count() == 0 75 | 76 | 77 | class Item(BaseModel): 78 | type = IntegerField(default=1) 79 | character = ForeignKeyField(Character, related_name='inventoryitems') 80 | itemid = IntegerField() 81 | inventory = IntegerField() 82 | slot = IntegerField() 83 | quantity = IntegerField(default=1) 84 | petid = IntegerField(default=-1) 85 | expiration = BigIntegerField(default=-1) 86 | gift_from = CharField(max_length=26, null=True) 87 | scroll_slots = IntegerField(default=0) 88 | level = IntegerField(default=0) 89 | str = IntegerField(default=0) 90 | dex = IntegerField(default=0) 91 | int = IntegerField(default=0) 92 | luk = IntegerField(default=0) 93 | hp = IntegerField(default=0) 94 | mp = IntegerField(default=0) 95 | watk = IntegerField(default=0) 96 | matk = IntegerField(default=0) 97 | wdef = IntegerField(default=0) 98 | mdef = IntegerField(default=0) 99 | acc = IntegerField(default=0) 100 | avoid = IntegerField(default=0) 101 | hands = IntegerField(default=0) 102 | speed = IntegerField(default=0) 103 | jump = IntegerField(default=0) 104 | locked = IntegerField(default=0) 105 | vicious = IntegerField(default=0) 106 | itemlevel = IntegerField(default=1) 107 | itemexp = IntegerField(default=0) 108 | ringid = IntegerField(default=-1) 109 | flag = IntegerField(default=0) 110 | 111 | class Meta: 112 | db_table = 'inventory_item' 113 | 114 | @classmethod 115 | def create(cls, itemid, character, slot, *args, **kwargs): 116 | inventory = gamelogicutils.get_inventory(itemid) 117 | item = Item() 118 | if inventory == enums.Inventory.EQUIP: 119 | sequip = staticmodels.ItemEquipData.from_id(itemid=itemid) 120 | item = Item( 121 | character=character, 122 | slot=slot, 123 | inventory=inventory, 124 | itemid=itemid, 125 | str=sequip.str, 126 | dex=sequip.dex, 127 | int=sequip.int, 128 | luk=sequip.luk, 129 | hp=sequip.hp, 130 | mp=sequip.mp, 131 | watk=sequip.watk, 132 | wdef=sequip.wdef, 133 | matk=sequip.matk, 134 | mdef=sequip.mdef, 135 | acc=sequip.acc, 136 | avoid=sequip.avoid, 137 | hands=sequip.hands, 138 | jump=sequip.jump, 139 | speed=sequip.jump, 140 | *args, **kwargs 141 | ) 142 | item.save() 143 | elif inventory == enums.Inventory.ETC: 144 | return super(Item, item).create( 145 | itemid=itemid, 146 | character=character, 147 | slot=slot, 148 | inventory=inventory, 149 | *args, **kwargs 150 | ) 151 | else: 152 | raise Exception("Item not implemented yet") 153 | return item 154 | 155 | @classmethod 156 | def get_inventory_for(cls, character, inventory): 157 | return (cls 158 | .select(cls.itemid, cls.slot) 159 | .where((cls.inventory == inventory) & 160 | (cls.character == character)) 161 | ) 162 | 163 | @classmethod 164 | def get_equips_for(cls, character): 165 | return cls.get_inventory_for(character, enums.Inventory.EQUIP) 166 | 167 | @classmethod 168 | def get_etcs_for(cls, character): 169 | return cls.get_inventory_for(character, enums.Inventory.ETC) 170 | 171 | 172 | class Keymap(BaseModel): 173 | character = ForeignKeyField(Character, related_name='keymaps') 174 | key = IntegerField(default=0) 175 | type = IntegerField(default=0) 176 | action = IntegerField(default=0) 177 | 178 | @classmethod 179 | def for_character(cls, character): 180 | keymap = { 181 | x.key: (x.type, x.action) 182 | for x in ( 183 | cls.select(cls.key, cls.type, cls.action) 184 | .where(cls.character == character) 185 | ) 186 | } 187 | for key in xrange(90): 188 | if key in keymap: 189 | yield keymap[key] 190 | else: 191 | yield (0, 0) 192 | 193 | @classmethod 194 | def handle_changes(cls, changes, character): 195 | inserts, deletes = [], [] 196 | for k, t, a in changes: 197 | if t != 0: 198 | inserts.append( 199 | {'key': k, 'type': t, 'action': a, 'character': character} 200 | ) 201 | else: 202 | deletes.append(k) 203 | cls.insert_many(inserts).execute() 204 | cls.delete().where( 205 | (cls.character == character) & (cls.key << deletes) 206 | ).execute() 207 | 208 | @classmethod 209 | def create_default(cls, character): 210 | keymaps = ( 211 | { 212 | 'key': key, 213 | 'type': ktype, 214 | 'action': action, 215 | 'character': character 216 | } for key, (ktype, action) in settings.DEFAULT_KEYMAP 217 | ) 218 | cls.insert_many(keymaps) 219 | -------------------------------------------------------------------------------- /pythonstory/channel/npc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martolini/pythonstory/121701004fd7f2e326c4969e8400958d39c74f76/pythonstory/channel/npc/__init__.py -------------------------------------------------------------------------------- /pythonstory/channel/npc/handlers.py: -------------------------------------------------------------------------------- 1 | from . import packets as npcpackets 2 | 3 | 4 | def npc_action(packet, client): 5 | size = packet.bytes_available 6 | if size == 6: 7 | i = packet.read_int() 8 | s = packet.read_short() 9 | client.send(npcpackets.npc_talk(i, s)) 10 | elif size > 6: 11 | arr = [packet.read_byte() for _ in xrange(size - 9)] 12 | client.send(npcpackets.npc_move(arr)) 13 | -------------------------------------------------------------------------------- /pythonstory/channel/npc/packets.py: -------------------------------------------------------------------------------- 1 | from pythonstory.common.decorators import packet 2 | from pythonstory.common import sendopcodes 3 | 4 | 5 | @packet(sendopcodes.SPAWN_NPC) 6 | def spawn_npc(builder, npc, show=True): 7 | (builder 8 | .write_int(npc.lifeid) 9 | .write_int(npc.lifeid) 10 | .write_short(npc.x_pos) 11 | .write_short(npc.y_pos) 12 | .write(npc.flags != 'faces_left') 13 | .write_short(npc.foothold) 14 | .write_short(npc.min_click_pos) 15 | .write_short(npc.max_click_pos) 16 | .write(show) 17 | ) 18 | return builder.get_packet() 19 | 20 | 21 | @packet(sendopcodes.SPAWN_NPC_REQUEST_CONTROLLER) 22 | def control_npc(builder, npc, minimap=True): 23 | (builder 24 | .write(1) 25 | .write_int(npc.lifeid) 26 | .write_int(npc.lifeid) 27 | .write_short(npc.x_pos) 28 | .write_short(npc.y_pos) 29 | .write(npc.flags != 'faces_left') 30 | .write_short(npc.foothold) 31 | .write_short(npc.min_click_pos) 32 | .write_short(npc.max_click_pos) 33 | .write(minimap) 34 | ) 35 | return builder.get_packet() 36 | 37 | 38 | @packet(sendopcodes.NPC_ACTION) 39 | def npc_talk(builder, i, s): 40 | (builder 41 | .write_int(i) 42 | .write_short(s) 43 | ) 44 | return builder.get_packet() 45 | 46 | 47 | @packet(sendopcodes.NPC_ACTION) 48 | def npc_move(builder, arr): 49 | builder.write_array(arr) 50 | return builder.get_packet() 51 | -------------------------------------------------------------------------------- /pythonstory/channel/packethelpers.py: -------------------------------------------------------------------------------- 1 | from ..common import enums, packethelpers as commonhelpers, gamelogicutils 2 | from .models import Item 3 | from pythonstory.channel.quest.models import Quest 4 | 5 | 6 | def add_char_info(builder, character): 7 | (builder 8 | .write_long(-1) 9 | .write(0) 10 | ) 11 | commonhelpers.add_char_stats(builder, character) 12 | (builder 13 | .write(0) # No buddies :( 14 | .write(0) # No linked name 15 | .write_int(character.meso) 16 | ) 17 | add_inventor_info(builder, character) 18 | add_skill_info(builder, character) 19 | Quest.connect_data(builder, character) 20 | add_minigame_info(builder, character) 21 | add_ring_info(builder, character) 22 | add_teleport_info(builder, character) 23 | add_monsterbook_info(builder, character) 24 | add_new_year_info(builder, character) 25 | add_area_info(builder, character) 26 | builder.write_short(0) 27 | return 28 | 29 | 30 | def add_inventor_info(builder, character): 31 | for i in xrange(5): # Write inventory slots 32 | builder.write(24) 33 | builder.write_long(enums.TimeType.ZERO_TIME) 34 | equips = Item.get_equips_for(character) 35 | for equip in equips: 36 | if -100 < equip.slot < 0: 37 | add_item_info(builder, equip) 38 | builder.write_short(0) 39 | 40 | for equip in equips: 41 | if equip.slot < -100: 42 | add_item_info(builder, equip) 43 | builder.write_short(0) 44 | 45 | for equip in equips: 46 | if equip.slot > 0: 47 | add_item_info(builder, equip) 48 | 49 | builder.write_int(0) # Use inventory 50 | builder.write(0) # Setup inv 51 | builder.write(0) 52 | # ETC INV 53 | for item in Item.get_etcs_for(character): 54 | add_item_info(builder, item) 55 | builder.write(0) 56 | return 57 | 58 | 59 | def add_item_info(builder, item, zeroslot=False): 60 | cash = gamelogicutils.is_cashslot(item.slot) 61 | pet = item.petid > -1 62 | equip = gamelogicutils.is_equip(item.itemid) 63 | slot = abs(item.slot) 64 | 65 | if not zeroslot: 66 | if equip: 67 | builder.write_short(slot - 100 if slot > 100 else slot) 68 | else: 69 | builder.write(slot + 1) 70 | (builder 71 | .write(1 if equip else 2) # 3 if pet? 72 | .write_int(item.itemid) 73 | .write(0) 74 | ) 75 | if cash: 76 | pass # Ignore for now 77 | builder.write_long(enums.TimeType.DEFAULT_TIME) 78 | if pet: 79 | return # Ignore 80 | if not equip: 81 | (builder 82 | .write_short(item.quantity) 83 | .write_string("") 84 | .write_short(item.flag) 85 | ) 86 | # Add rechargables 87 | return 88 | 89 | (builder 90 | .write(item.scroll_slots) 91 | .write(item.level) 92 | .write_short(item.str) 93 | .write_short(item.dex) 94 | .write_short(item.int) 95 | .write_short(item.luk) 96 | .write_short(item.hp) 97 | .write_short(item.mp) 98 | .write_short(item.watk) 99 | .write_short(item.matk) 100 | .write_short(item.wdef) 101 | .write_short(item.mdef) 102 | .write_short(item.acc) 103 | .write_short(item.avoid) 104 | .write_short(item.hands) 105 | .write_short(item.speed) 106 | .write_short(item.jump) 107 | .write_string("") # Owner 108 | .write_short(item.flag) 109 | ) 110 | if cash: 111 | pass 112 | else: 113 | (builder 114 | .write(0) 115 | .write(item.level) 116 | .write_short(0) 117 | .write_short(item.itemexp) 118 | .write_int(item.vicious) 119 | .write_long(0) 120 | ) 121 | (builder 122 | .write_long(enums.TimeType.ZERO_TIME) 123 | .write_int(-1) 124 | ) 125 | return 126 | 127 | 128 | def add_skill_info(builder, character): 129 | (builder 130 | .write(0) # Start of skills 131 | .write_short(0) # Size of skills 132 | .write_short(0) # Cooldown size 133 | ) 134 | 135 | 136 | def add_minigame_info(builder, character): 137 | builder.write_short(0) 138 | return 139 | 140 | 141 | def add_ring_info(builder, character): 142 | (builder 143 | .write_short(0) # CRUSH RINGS SIZE 144 | .write_short(0) # FRIENDSSHIP RING SIZE 145 | .write_short(0) # MARRIAGE ? No thanks. 146 | ) 147 | return 148 | 149 | 150 | def add_teleport_info(builder, character): 151 | for i in xrange(5): 152 | builder.write_int(999999999) # TELE ROCK MAPS 153 | for i in xrange(10): 154 | builder.write_int(999999999) # VIP TELE ROCK MAPS 155 | return 156 | 157 | 158 | def add_monsterbook_info(builder, character): 159 | (builder 160 | .write_int(0) # MONSTER BOOK COVER 161 | .write(0) 162 | .write_short(0) # 0 monsterbook cards 163 | ) 164 | return 165 | 166 | 167 | def add_new_year_info(builder, character): 168 | builder.write_short(0) 169 | return 170 | 171 | 172 | def add_area_info(builder, character): 173 | builder.write_short(0) # null area info 174 | return 175 | -------------------------------------------------------------------------------- /pythonstory/channel/packets.py: -------------------------------------------------------------------------------- 1 | from ..common.decorators import packet 2 | from ..common import sendopcodes 3 | from . import packethelpers 4 | from ..common.packethelpers import maple_time 5 | from .models import Keymap 6 | 7 | import random 8 | import time 9 | 10 | 11 | @packet(sendopcodes.SET_FIELD) 12 | def char_info(builder, character, channel): 13 | (builder 14 | .write_int(channel - 1) 15 | .write(1) 16 | .write(1) 17 | .write_short(0) 18 | ) 19 | for i in xrange(3): 20 | builder.write_int(random.randint(1, 10000000)) 21 | packethelpers.add_char_info(builder, character) 22 | builder.write_long(maple_time(time.time())) 23 | return builder.get_packet() 24 | 25 | 26 | @packet(sendopcodes.KEYMAP) 27 | def keymap(builder, character): 28 | builder.write(0) 29 | for ktype, action in Keymap.for_character(character): 30 | (builder 31 | .write(ktype) 32 | .write_int(action) 33 | ) 34 | return builder.get_packet() 35 | 36 | 37 | @packet(sendopcodes.MACRO_SYS_DATA_INIT) 38 | def skillmacros(builder, character): 39 | builder.write(0) 40 | return builder.get_packet() 41 | 42 | 43 | @packet(sendopcodes.BUDDYLIST) 44 | def buddy_list(builder, character): 45 | (builder 46 | .write(7) 47 | .write(0) # 0 Buddies :x 48 | ) 49 | return builder.get_packet() 50 | 51 | 52 | @packet(sendopcodes.FAMILY_PRIVILEGE_LIST) 53 | def load_family(builder, character): 54 | builder.write_int(11) 55 | rep_cost = (3, 5, 7, 8, 10, 12, 15, 20, 25, 40, 50) 56 | for i in xrange(11): 57 | (builder 58 | .write((i % 2) + 1 if i > 4 else i) 59 | .write_int(rep_cost[i] * 100) 60 | .write_int(1) 61 | .write_string("Title {}".format(i)) 62 | .write_string("Description {}".format(i)) 63 | ) 64 | return builder.get_packet() 65 | -------------------------------------------------------------------------------- /pythonstory/channel/player/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martolini/pythonstory/121701004fd7f2e326c4969e8400958d39c74f76/pythonstory/channel/player/__init__.py -------------------------------------------------------------------------------- /pythonstory/channel/player/handlers.py: -------------------------------------------------------------------------------- 1 | from ..map import handlers as maphandlers 2 | from ..map import models as mapmodels 3 | from ..models import Keymap 4 | from .. import commands 5 | 6 | 7 | def move_player(packet, client): 8 | packet.skip(9) 9 | x, y, foothold, stance = maphandlers.parse_movement(packet) 10 | # client.character.move(x, y, foothold, stance) 11 | # client.send_map() # SOME PACKET, who cares as i'm the only one lol 12 | 13 | 14 | def change_map(packet, client): 15 | if len(packet.data) == 0: 16 | # CASH SHOP 17 | return 18 | packet.read_byte() 19 | packet.read_int() # Target map, use for something? 20 | startwp = packet.read_maplestring() 21 | currentmap = mapmodels.Map.get( 22 | mapid=client.character.map, 23 | channel=client.factory.key) 24 | portal = currentmap.get_portal_from_string(startwp) 25 | packet.read_byte() 26 | packet.read_short() > 0 # moople wheel 27 | nextmap = mapmodels.Map.get( 28 | mapid=portal.destination, 29 | channel=client.factory.key 30 | ) 31 | nextportalid = nextmap.get_portal_from_string(portal.destination_label).id 32 | client.change_map(nextmap.id, nextportalid) 33 | 34 | 35 | def change_keymap(packet, client): 36 | packet.read_int() 37 | numchanges = packet.read_int() 38 | changes = [] 39 | for _ in xrange(numchanges): 40 | key = packet.read_int() 41 | ktype = packet.read_byte() 42 | action = packet.read_int() 43 | changes.append((key, ktype, action)) 44 | Keymap.handle_changes(changes, client.character) 45 | 46 | 47 | def handle_chat(packet, client): 48 | message = packet.read_maplestring() 49 | if message[0] == '@': 50 | splitted = message[1:].split(' ') 51 | command = splitted[0] 52 | args = splitted[1:] 53 | try: 54 | func = getattr(commands, command) 55 | func(client, *args) 56 | except AttributeError: 57 | print "No command {}".format(command) 58 | -------------------------------------------------------------------------------- /pythonstory/channel/player/packets.py: -------------------------------------------------------------------------------- 1 | from pythonstory.common.decorators import packet 2 | from pythonstory.common import sendopcodes 3 | from pythonstory.common.packethelpers import maple_time 4 | 5 | import time 6 | 7 | 8 | @packet(sendopcodes.SET_FIELD) 9 | def change_map(builder, destinationid, character, channel, spawnpoint=0): 10 | (builder 11 | .write_int(channel - 1) 12 | .write_int(0) 13 | .write(0) 14 | .write_int(destinationid) 15 | .write(spawnpoint) 16 | .write_short(character.hp) 17 | .write(0) 18 | .write_long(maple_time(time.time())) 19 | ) 20 | return builder.get_packet() 21 | -------------------------------------------------------------------------------- /pythonstory/channel/processor.py: -------------------------------------------------------------------------------- 1 | from ..common import processor, recvopcodes 2 | from ..common import handlers as commonhandlers 3 | from . import handlers as channelhandlers 4 | from .player import handlers as playerhandlers 5 | from .quest import handlers as questhandlers 6 | from .npc import handlers as npchandlers 7 | 8 | 9 | class ChannelPacketProcessor(processor.BasePacketProcessor): 10 | ignored_opcodes = ( 11 | recvopcodes.CHANGE_MAP_SPECIAL, 12 | ) 13 | 14 | handlers = { 15 | recvopcodes.PONG: commonhandlers.pong, 16 | recvopcodes.PLAYER_LOAD: channelhandlers.load_player, 17 | recvopcodes.MOVE_PLAYER: playerhandlers.move_player, 18 | recvopcodes.CHANGE_MAP: playerhandlers.change_map, 19 | recvopcodes.CHANGE_KEYMAP: playerhandlers.change_keymap, 20 | recvopcodes.GENERAL_CHAT: playerhandlers.handle_chat, 21 | recvopcodes.QUEST_ACTION: questhandlers.quest_action, 22 | recvopcodes.NPC_ACTION: npchandlers.npc_action 23 | } 24 | -------------------------------------------------------------------------------- /pythonstory/channel/protocol.py: -------------------------------------------------------------------------------- 1 | from ..common.protocol import MapleProtocol 2 | from .processor import ChannelPacketProcessor 3 | from .map import models as mapmodels 4 | from .player import packets as playerpackets 5 | 6 | 7 | class ChannelProtocol(MapleProtocol): 8 | processor = ChannelPacketProcessor 9 | 10 | def __init__(self, *args, **kwargs): 11 | self.character = None 12 | super(ChannelProtocol, self).__init__(*args, **kwargs) 13 | 14 | @property 15 | def channel(self): 16 | return self.factory.key 17 | 18 | def send_map(self, *args, **kwargs): 19 | print 'Should send to map' 20 | 21 | def change_map(self, mapid, spawnpoint=0): 22 | currentmap = mapmodels.Map.get( 23 | self.character.map, 24 | self.channel) 25 | nextmap = mapmodels.Map.get( 26 | mapid, 27 | self.channel) 28 | currentmap.remove_client(self) 29 | self.character.map = nextmap.id 30 | self.send(playerpackets.change_map( 31 | nextmap.id, 32 | self.character, 33 | self.channel, 34 | spawnpoint 35 | )) 36 | nextmap.add_client(self) 37 | 38 | def connectionLost(self, reason): 39 | if self.character is not None: 40 | self.character.save() 41 | mapmodels.Map.get( 42 | mapid=self.character.map, 43 | channel=self.factory.key 44 | ).remove_client(self) 45 | return super(ChannelProtocol, self).connectionLost(reason) 46 | -------------------------------------------------------------------------------- /pythonstory/channel/quest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martolini/pythonstory/121701004fd7f2e326c4969e8400958d39c74f76/pythonstory/channel/quest/__init__.py -------------------------------------------------------------------------------- /pythonstory/channel/quest/handlers.py: -------------------------------------------------------------------------------- 1 | from .models import Quest 2 | from . import packets as questpackets 3 | 4 | 5 | def quest_action(packet, client): 6 | action = packet.read_byte() 7 | questid = packet.read_short() 8 | Quest.for_character(client.character, questid) 9 | print "Action {}".format(action) 10 | if action == 1: # Starting quest 11 | npcid = packet.read_int() 12 | if packet.bytes_available >= 4: 13 | packet.read_int() 14 | client.send(questpackets.accept_quest(questid, npcid)) 15 | client.send(questpackets.accept_quest_notice(questid)) 16 | elif action == 2: # Quest completed 17 | npcid = packet.read_int() 18 | Quest.complete(questid) 19 | client.send(questpackets.complete_quest(questid)) 20 | -------------------------------------------------------------------------------- /pythonstory/channel/quest/models.py: -------------------------------------------------------------------------------- 1 | from peewee import ForeignKeyField, IntegerField 2 | 3 | # from pythonstory.common.staticmodels import QuestData 4 | from pythonstory.common.models import BaseModel 5 | from pythonstory.channel.models import Character 6 | import time 7 | 8 | 9 | class Quest(BaseModel): 10 | character = ForeignKeyField(Character) 11 | questid = IntegerField() 12 | status = IntegerField(default=0) 13 | 14 | @classmethod 15 | def for_character(cls, character, questid): 16 | quest, _ = cls.get_or_create( 17 | character=character.id, 18 | questid=questid 19 | ) 20 | return quest 21 | 22 | @classmethod 23 | def connect_data(cls, builder, character): 24 | qset = Quest.select().where(cls.character == character) 25 | active = [q for q in qset if q.status == 0] 26 | completed = [q for q in qset if q.status == 1] 27 | builder.write_short(len(active)) 28 | for quest in active: 29 | (builder 30 | .write_short(quest.questid) 31 | .write_string("") 32 | ) 33 | builder.write_short(len(completed)) 34 | for quest in completed: 35 | (builder 36 | .write_short(quest.questid) 37 | .write_long(int(time.time())) 38 | ) 39 | 40 | @classmethod 41 | def complete(cls, questid): 42 | cls.update(status=1).where(cls.questid == questid).execute() 43 | -------------------------------------------------------------------------------- /pythonstory/channel/quest/packets.py: -------------------------------------------------------------------------------- 1 | from pythonstory.common.decorators import packet 2 | from pythonstory.common import sendopcodes 3 | import time 4 | 5 | 6 | @packet(sendopcodes.UPDATE_QUEST_INFO) 7 | def accept_quest(builder, questid, npcid): 8 | (builder 9 | .write(8) 10 | .write_short(questid) 11 | .write_int(npcid) 12 | .write_int(0) 13 | ) 14 | return builder.get_packet() 15 | 16 | 17 | @packet(sendopcodes.SHOW_STATUS_INFO) 18 | def accept_quest_notice(builder, questid): 19 | (builder 20 | .write(1) 21 | .write_short(questid) 22 | .write(1) 23 | .write_int(0) 24 | .write_int(0) 25 | .write_short(0) 26 | ) 27 | return builder.get_packet() 28 | 29 | 30 | @packet(sendopcodes.SHOW_STATUS_INFO) 31 | def complete_quest(builder, questid, completion_time=None): 32 | if completion_time is None: 33 | completion_time = int(time.time()) 34 | (builder 35 | .write(1) 36 | .write_short(questid) 37 | .write(2) 38 | .write_long(completion_time) 39 | ) 40 | return builder.get_packet() 41 | -------------------------------------------------------------------------------- /pythonstory/common/__init__.py: -------------------------------------------------------------------------------- 1 | import settings 2 | import decoder 3 | from packetbuilder import PacketBuilder 4 | from packetreader import PacketReader 5 | import sendopcodes 6 | import recvopcodes 7 | import processor 8 | -------------------------------------------------------------------------------- /pythonstory/common/bitutils.py: -------------------------------------------------------------------------------- 1 | def roll_left(inn, count): 2 | tmp = inn & 0xFF 3 | tmp = tmp << (count % 8) 4 | return ((tmp & 0xFF) | (tmp >> 8)) 5 | 6 | 7 | def roll_right(inn, count): 8 | tmp = inn & 0xFF 9 | tmp = (tmp << 8) >> (count % 8) 10 | return (tmp & 0xFF) | (tmp >> 8) 11 | 12 | 13 | def unsigned_right_shift(num, arg): 14 | return (num % 0x100000000) >> arg 15 | -------------------------------------------------------------------------------- /pythonstory/common/decoder.py: -------------------------------------------------------------------------------- 1 | import random 2 | import struct 3 | from Crypto.Cipher import AES 4 | from . import bitutils 5 | from threading import Lock 6 | 7 | 8 | aeskey = [ 9 | 0x13, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 10 | 0xB4, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 11 | 0x33, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00 12 | ] 13 | update_matrix = [ 14 | 0xEC, 0x3F, 0x77, 0xA4, 0x45, 0xD0, 0x71, 0xBF, 0xB7, 0x98, 0x20, 0xFC, 15 | 0x4B, 0xE9, 0xB3, 0xE1, 0x5C, 0x22, 0xF7, 0x0C, 0x44, 0x1B, 0x81, 0xBD, 16 | 0x63, 0x8D, 0xD4, 0xC3, 0xF2, 0x10, 0x19, 0xE0, 0xFB, 0xA1, 0x6E, 0x66, 17 | 0xEA, 0xAE, 0xD6, 0xCE, 0x06, 0x18, 0x4E, 0xEB, 0x78, 0x95, 0xDB, 0xBA, 18 | 0xB6, 0x42, 0x7A, 0x2A, 0x83, 0x0B, 0x54, 0x67, 0x6D, 0xE8, 0x65, 0xE7, 19 | 0x2F, 0x07, 0xF3, 0xAA, 0x27, 0x7B, 0x85, 0xB0, 0x26, 0xFD, 0x8B, 0xA9, 20 | 0xFA, 0xBE, 0xA8, 0xD7, 0xCB, 0xCC, 0x92, 0xDA, 0xF9, 0x93, 0x60, 0x2D, 21 | 0xDD, 0xD2, 0xA2, 0x9B, 0x39, 0x5F, 0x82, 0x21, 0x4C, 0x69, 0xF8, 0x31, 22 | 0x87, 0xEE, 0x8E, 0xAD, 0x8C, 0x6A, 0xBC, 0xB5, 0x6B, 0x59, 0x13, 0xF1, 23 | 0x04, 0x00, 0xF6, 0x5A, 0x35, 0x79, 0x48, 0x8F, 0x15, 0xCD, 0x97, 0x57, 24 | 0x12, 0x3E, 0x37, 0xFF, 0x9D, 0x4F, 0x51, 0xF5, 0xA3, 0x70, 0xBB, 0x14, 25 | 0x75, 0xC2, 0xB8, 0x72, 0xC0, 0xED, 0x7D, 0x68, 0xC9, 0x2E, 0x0D, 0x62, 26 | 0x46, 0x17, 0x11, 0x4D, 0x6C, 0xC4, 0x7E, 0x53, 0xC1, 0x25, 0xC7, 0x9A, 27 | 0x1C, 0x88, 0x58, 0x2C, 0x89, 0xDC, 0x02, 0x64, 0x40, 0x01, 0x5D, 0x38, 28 | 0xA5, 0xE2, 0xAF, 0x55, 0xD5, 0xEF, 0x1A, 0x7C, 0xA7, 0x5B, 0xA6, 0x6F, 29 | 0x86, 0x9F, 0x73, 0xE6, 0x0A, 0xDE, 0x2B, 0x99, 0x4A, 0x47, 0x9C, 0xDF, 30 | 0x09, 0x76, 0x9E, 0x30, 0x0E, 0xE4, 0xB2, 0x94, 0xA0, 0x3B, 0x34, 0x1D, 31 | 0x28, 0x0F, 0x36, 0xE3, 0x23, 0xB4, 0x03, 0xD8, 0x90, 0xC8, 0x3C, 0xFE, 32 | 0x5E, 0x32, 0x24, 0x50, 0x1F, 0x3A, 0x43, 0x8A, 0x96, 0x41, 0x74, 0xAC, 33 | 0x52, 0x33, 0xF0, 0xD9, 0x29, 0x80, 0xB1, 0x16, 0xD3, 0xAB, 0x91, 0xB9, 34 | 0x84, 0x7F, 0x61, 0x1E, 0xCF, 0xC5, 0xD1, 0x56, 0x3D, 0xCA, 0xF4, 0x05, 35 | 0xC6, 0xE5, 0x08, 0x49 36 | ] 37 | 38 | 39 | def AESencrypt(data): 40 | cipher = AES.new(bytes(bytearray(aeskey))) 41 | return struct.unpack('!16B', cipher.encrypt(bytes(bytearray(data)))) 42 | 43 | 44 | class Decoder: 45 | def __init__(self): 46 | self.counter = 0 47 | self.lock = Lock() 48 | self.send = [82, 48, 120, random.randint(1, 122)] 49 | self.receive = [70, 114, 122, random.randint(1, 122)] 50 | self.receive_mapleversion = 83 51 | self.send_mapleversion = 0xFFFF - 83 52 | self.send_mapleversion = ( 53 | ((self.send_mapleversion >> 8) & 0xFF) | 54 | ((self.send_mapleversion << 8) & 0xFF00) 55 | ) 56 | self.receive_mapleversion = ( 57 | ((self.receive_mapleversion >> 8) & 0xFF) | 58 | ((self.receive_mapleversion << 8) & 0xFF00) 59 | ) 60 | 61 | def encode(self, data): 62 | self.lock.acquire() 63 | header = self.get_packet_header(len(data)) 64 | # Maple custom encryption 65 | for j in xrange(0, 6): 66 | remember = 0 67 | data_length = len(data) & 0xFF 68 | if j % 2 == 0: 69 | for i in xrange(len(data)): 70 | cur = data[i] 71 | cur = bitutils.roll_left(cur, 3) 72 | cur += data_length 73 | cur ^= remember 74 | remember = cur 75 | cur = bitutils.roll_right(cur, data_length & 0xFF) 76 | cur = ~cur & 0xFF 77 | cur += 0x48 78 | data_length -= 1 79 | data[i] = cur 80 | else: 81 | for i in reversed(xrange(len(data))): 82 | cur = data[i] 83 | cur = bitutils.roll_left(cur, 4) 84 | cur += data_length 85 | cur ^= remember 86 | remember = cur 87 | cur ^= 0x13 88 | cur = bitutils.roll_right(cur, 3) 89 | data_length -= 1 90 | data[i] = cur 91 | 92 | data = self.maple_aes(data, self.send) 93 | self.encodeUpdate() 94 | self.lock.release() 95 | return header + data 96 | 97 | def maple_aes(self, data, iv): 98 | remaining = len(data) 99 | length = 1456 100 | start = 0 101 | while remaining > 0: 102 | receive = iv * 4 103 | if remaining < length: 104 | length = remaining 105 | for x in xrange(start, start+length): 106 | if (x - start) % len(receive) == 0: 107 | receive = AESencrypt(receive) 108 | data[x] ^= receive[(x - start) % len(receive)] 109 | start += length 110 | remaining -= length 111 | length = 0x5B4 112 | return data 113 | 114 | def decode(self, data): 115 | self.lock.acquire() 116 | data = list(struct.unpack('!%sB' % len(data), data)) 117 | data = self.maple_aes(data, self.receive) 118 | self.decodeUpdate() 119 | 120 | # Maple Custom Decryption 121 | for j in range(1, 7): 122 | remember = 0 123 | data_length = len(data) & 0xFF 124 | next_remember = 0 125 | if j % 2 == 0: 126 | for i in xrange(len(data)): 127 | cur = data[i] 128 | cur -= 0x48 129 | cur = ~cur & 0xFF 130 | cur = bitutils.roll_left(cur, data_length & 0xFF) 131 | next_remember = cur 132 | cur ^= remember 133 | remember = next_remember 134 | cur -= data_length 135 | cur = bitutils.roll_right(cur, 3) 136 | data[i] = cur 137 | data_length -= 1 138 | else: 139 | for i in reversed(xrange(len(data))): 140 | cur = data[i] 141 | cur = bitutils.roll_left(cur, 3) 142 | cur ^= 0x13 143 | next_remember = cur 144 | cur ^= remember 145 | remember = next_remember 146 | cur -= data_length 147 | cur = bitutils.roll_right(cur, 4) 148 | data[i] = cur 149 | data_length -= 1 150 | self.lock.release() 151 | return data 152 | 153 | def encodeUpdate(self): 154 | self.updateIV(self.send) 155 | 156 | def decodeUpdate(self): 157 | self.updateIV(self.receive) 158 | 159 | def updateIV(self, matrix): 160 | arr = [0xf2, 0x53, 0x50, 0xc6] 161 | for x in range(4): 162 | self.rotate(matrix[x], arr) 163 | matrix[:] = arr 164 | 165 | def rotate(self, input_byte, arr): 166 | elina = arr[1] 167 | anna = input_byte 168 | moritz = update_matrix[elina & 0xFF] 169 | moritz -= input_byte 170 | arr[0] += moritz 171 | moritz = arr[2] 172 | moritz ^= update_matrix[(int(anna) & 0xFF)] 173 | elina -= int(moritz) & 0xFF 174 | arr[1] = elina 175 | elina = arr[3] 176 | moritz = elina 177 | elina -= int(arr[0] & 0xFF) 178 | moritz = update_matrix[int(moritz & 0xFF)] 179 | moritz += input_byte 180 | moritz ^= arr[2] 181 | arr[2] = moritz 182 | elina += int(update_matrix[int(anna) & 0xFF]) & 0xFF 183 | arr[3] = elina 184 | merry = int(arr[0]) & 0xFF 185 | merry |= (arr[1] << 8) & 0xFF00 186 | merry |= (arr[2] << 16) & 0xFF0000 187 | merry |= (arr[3] << 24) & 0xFF000000 188 | ret_value = merry 189 | ret_value = bitutils.unsigned_right_shift(ret_value, 0x1d) 190 | merry = merry << 3 191 | ret_value = ret_value | merry 192 | arr[0] = ret_value & 0xFF 193 | arr[1] = (ret_value >> 8) & 0xFF 194 | arr[2] = (ret_value >> 16) & 0xFF 195 | arr[3] = (ret_value >> 24) & 0xFF 196 | return True 197 | 198 | # SHOULD BE [-44, -116, -42, -116] 199 | # [212, 140, 214, 140] 200 | def get_packet_header(self, length): 201 | iiv = self.send[3] & 0xFF 202 | iiv |= (self.send[2] << 8) & 0xFF00 203 | iiv ^= self.send_mapleversion 204 | mlength = ((length << 8) & 0xFF00) | (bitutils.unsigned_right_shift(length, 8)) 205 | xoredIv = iiv ^ mlength 206 | return [ 207 | bitutils.unsigned_right_shift(iiv, 8) & 0xFF, 208 | iiv & 0xFF, 209 | (bitutils.unsigned_right_shift(xoredIv, 8) & 0xFF), 210 | xoredIv & 0xFF 211 | ] 212 | 213 | def get_packet_length(self, headerint): 214 | packetlength = (headerint >> 16) ^ (headerint & 0xFFFF) 215 | packetlength = ((packetlength << 8) & 0xFF00) | ((packetlength >> 8) & 0xFF) 216 | return packetlength 217 | 218 | def check_header(self, header): 219 | header = ( 220 | header >> 24 & 0xFF, 221 | header >> 16 & 0xFF 222 | ) 223 | first = (header[0] ^ self.receive[2]) & 0xFF 224 | second = (self.receive_mapleversion >> 8) & 0xFF 225 | third = (header[1] ^ self.receive[3]) & 0xFF 226 | fourth = self.receive_mapleversion & 0xFF 227 | return first == second and third == fourth 228 | -------------------------------------------------------------------------------- /pythonstory/common/decorators.py: -------------------------------------------------------------------------------- 1 | from . import packetbuilder 2 | 3 | def packet(opcode): 4 | def decorator(func): 5 | def wrapper(*args, **kwargs): 6 | pb = packetbuilder.PacketBuilder() 7 | pb.write_short(opcode) 8 | return func(pb, *args, **kwargs) 9 | return wrapper 10 | return decorator 11 | -------------------------------------------------------------------------------- /pythonstory/common/enums.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class Inventory(IntEnum): 5 | EQUIP = 1 6 | USE = 2 7 | SETUP = 3 8 | ETC = 4 9 | CASH = 5 10 | 11 | MINSLOTINV = 24 12 | MAXSLOTINV = 100 13 | MINSLOTSTORAGE = 4 14 | MAXSLOTSTORAGE = 100 15 | 16 | 17 | class MapleJob(IntEnum): 18 | BEGINNER = 0 19 | 20 | WARRIOR = 100 21 | 22 | FIGHTER = 110 23 | CRUSADER = 111 24 | HERO = 112 25 | 26 | PAGE = 120 27 | WHITEKNIGHT = 121 28 | PALADIN = 122 29 | 30 | SPEARMAN = 130 31 | DRAGONKNIGHT = 131 32 | DARKKNIGHT = 132 33 | 34 | MAGICIAN = 200 35 | 36 | FP_WIZARD = 210 37 | FP_MAGE = 211 38 | FP_ARCHMAGE = 212 39 | 40 | IL_WIZARD = 220 41 | IL_MAGE = 221 42 | IL_ARCHMAGE = 222 43 | 44 | CLERIC = 230 45 | PRIEST = 231 46 | BISHOP = 232 47 | 48 | BOWMAN = 300 49 | 50 | HUNTER = 310 51 | RANGER = 311 52 | BOWMASTER = 312 53 | 54 | CROSSBOWMAN = 320 55 | SNIPER = 321 56 | MARKSMAN = 322 57 | 58 | THIEF = 400 59 | 60 | ASSASSIN = 410 61 | HERMIT = 411 62 | NIGHTLORD = 412 63 | 64 | BANDIT = 420 65 | CHIEFBANDIT = 421 66 | SHADOWER = 422 67 | 68 | PIRATE = 500 69 | 70 | BRAWLER = 510 71 | MARAUDER = 511 72 | BUCCANEER = 512 73 | 74 | GUNSLINGER = 520 75 | OUTLAW = 521 76 | CORSAIR = 522 77 | 78 | MAPLELEAF_BRIGADIER = 800 79 | 80 | GM = 900 81 | SUPERGM = 910 82 | 83 | NOBLESSE = 1000 84 | 85 | DAWNWARRIOR1 = 1100 86 | DAWNWARRIOR2 = 1110 87 | DAWNWARRIOR3 = 1111 88 | DAWNWARRIOR4 = 1112 89 | 90 | BLAZEWIZARD1 = 1200 91 | BLAZEWIZARD2 = 1210 92 | BLAZEWIZARD3 = 1211 93 | BLAZEWIZARD4 = 1212 94 | 95 | WINDARCHER1 = 1300 96 | WINDARCHER2 = 1310 97 | WINDARCHER3 = 1311 98 | WINDARCHER4 = 1312 99 | 100 | NIGHTWALKER1 = 1400 101 | NIGHTWALKER2 = 1410 102 | NIGHTWALKER3 = 1411 103 | NIGHTWALKER4 = 1412 104 | 105 | THUNDERBREAKER1 = 1500 106 | THUNDERBREAKER2 = 1510 107 | THUNDERBREAKER3 = 1511 108 | THUNDERBREAKER4 = 1512 109 | 110 | LEGEND = 2000 111 | 112 | ARAN1 = 2100 113 | ARAN2 = 2110 114 | ARAN3 = 2111 115 | ARAN4 = 2112 116 | 117 | 118 | class EquipSlot(IntEnum): 119 | HELMET = 1 120 | FACE = 2 121 | EYE = 3 122 | EARRING = 4 123 | TOP = 5 124 | BOTTOM = 6 125 | SHOE = 7 126 | GLOVE = 8 127 | CAPE = 9 128 | SHIELD = 10 129 | WEAPON = 11 130 | RING1 = 12 131 | RING2 = 13 132 | PETEQUIP1 = 14 133 | RING3 = 15 134 | RING4 = 16 135 | PENDANT = 17 136 | MOUNT = 18 137 | SADDLE = 19 138 | PETCOLLAR = 20 139 | PETLABELRING1 = 21 140 | PETITEMPOUCH1 = 22 141 | PETMESOMAGNET1 = 23 142 | PETAUTOHP = 24 143 | PETAUTOMP = 25 144 | PETWINGBOOTS1 = 26 145 | PETBINOCULARS1 = 27 146 | PETMAGICSCALES1 = 28 147 | PETQUOTERING1 = 29 148 | PETEQUIP2 = 30 149 | PETLABELRING2 = 31 150 | PETQUOTERING2 = 32 151 | PETITEMPOUCH2 = 33 152 | PETMESOMAGNET2 = 34 153 | PETWINGBOOTS2 = 35 154 | PETBINOCULARS2 = 36 155 | PETMAGICSCALES2 = 37 156 | PETEQUIP3 = 38 157 | PETLABELRING3 = 39 158 | PETQUOTERING3 = 40 159 | PETITEMPOUCH3 = 41 160 | PETMESOMAGNET3 = 42 161 | PETWINGBOOTS3 = 43 162 | PETBINOCULARS3 = 44 163 | PETMAGICSCALES3 = 45 164 | PETITEMIGNORE1 = 46 165 | PETITEMIGNORE2 = 47 166 | PETITEMIGNORE3 = 48 167 | MEDAL = 49 168 | BELT = 50 169 | 170 | 171 | class TimeType(IntEnum): 172 | FT_UT_OFFSET = 116444592000000000L 173 | DEFAULT_TIME = 150842304000000000L 174 | ZERO_TIME = 94354848000000000L 175 | PERMANENT = 150841440000000000L 176 | -------------------------------------------------------------------------------- /pythonstory/common/factory.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import protocol 2 | 3 | class MapleFactory(protocol.Factory, object): 4 | protocol = None 5 | 6 | def __init__(self): 7 | self.connections = [] 8 | -------------------------------------------------------------------------------- /pythonstory/common/gamelogicutils.py: -------------------------------------------------------------------------------- 1 | from . import enums 2 | 3 | 4 | def get_inventory(id): 5 | return id / 1000000 6 | 7 | 8 | def is_cashslot(slot): 9 | return abs(slot) > 100 10 | 11 | 12 | def is_pet(id): 13 | return id / 100 * 100 == 5000000 14 | 15 | 16 | def is_equip(id): 17 | return get_inventory(id) == enums.Inventory.EQUIP 18 | -------------------------------------------------------------------------------- /pythonstory/common/handlers.py: -------------------------------------------------------------------------------- 1 | from time import time as timenow 2 | 3 | 4 | def pong(packet, client): 5 | client.latency = (timenow() - client.lastping) / 2 6 | -------------------------------------------------------------------------------- /pythonstory/common/helperclasses.py: -------------------------------------------------------------------------------- 1 | class Point: 2 | def __init__(self, x, y): 3 | self.x = x 4 | self.y = y 5 | 6 | 7 | class Rect: 8 | def __init__(self, p1, p2): 9 | self.p1 = p1 10 | self.p2 = p2 11 | -------------------------------------------------------------------------------- /pythonstory/common/mixins.py: -------------------------------------------------------------------------------- 1 | from pythonstory.channel.models import Character 2 | 3 | 4 | class CharacterCacheMixin(object): 5 | def __init__(self, *args, **kwargs): 6 | self.character_cache = {} 7 | super(CharacterCacheMixin, self).__init__(*args, **kwargs) 8 | 9 | def get_character(self, key): 10 | if key not in self.character_cache: 11 | c = Character.get(id=key) 12 | self.character_cache[key] = c 13 | return self.character_cache[key] 14 | 15 | def set_character(self, character): 16 | self.character_cache[character.id] = character 17 | -------------------------------------------------------------------------------- /pythonstory/common/models.py: -------------------------------------------------------------------------------- 1 | from peewee import Model 2 | from . import settings 3 | 4 | 5 | class BaseModel(Model): 6 | class Meta: 7 | database = settings.DATABASES['default'] 8 | -------------------------------------------------------------------------------- /pythonstory/common/packetbuilder.py: -------------------------------------------------------------------------------- 1 | class PacketBuilder: 2 | def __init__(self): 3 | self.arr = [] 4 | 5 | def write_short(self, val): 6 | self.arr.extend((val & 0xFF, val >> 8 & 0xFF)) 7 | return self 8 | 9 | def write(self, val): 10 | self.arr.append(val) 11 | return self 12 | 13 | def write_string(self, string, prepend_length=True): 14 | if prepend_length: 15 | self.write_short(len(string)) 16 | self.arr.extend((ord(c) for c in string)) 17 | return self 18 | 19 | def write_array(self, arr): 20 | self.arr.extend(arr) 21 | return self 22 | 23 | def write_int(self, val): 24 | self.arr.extend( 25 | ( 26 | val & 0xFF, 27 | val >> 8 & 0xFF, 28 | val >> 16 & 0xFF, 29 | val >> 24 & 0xFF 30 | ) 31 | ) 32 | return self 33 | 34 | def write_string_rightpad(self, string, pad, length): 35 | self.write_string( 36 | string + pad * (length - len(string)), 37 | prepend_length=False 38 | ) 39 | return self 40 | 41 | def write_long(self, val): 42 | self.arr.extend( 43 | ( 44 | val & 0xFF, 45 | val >> 8 & 0xFF, 46 | val >> 16 & 0xFF, 47 | val >> 24 & 0xFF, 48 | val >> 32 & 0xFF, 49 | val >> 40 & 0xFF, 50 | val >> 48 & 0xFF, 51 | val >> 56 & 0xFF 52 | ) 53 | ) 54 | return self 55 | 56 | def skip(self, num): 57 | self.arr.extend([0] * num) 58 | return self 59 | 60 | def get_packet(self): 61 | return self.arr 62 | -------------------------------------------------------------------------------- /pythonstory/common/packethelpers.py: -------------------------------------------------------------------------------- 1 | from . import enums 2 | 3 | 4 | def maple_time(ts): 5 | if ts == -1: 6 | return enums.TimeType.DEFAULT_TIME 7 | if ts == -2: 8 | return enums.TimeType.ZERO_TIME 9 | if ts == -3: 10 | return enums.TimeType.PERMANENT 11 | return int(ts) * 1000 * 10000 + enums.TimeType.FT_UT_OFFSET 12 | 13 | 14 | def add_char_stats(builder, character): 15 | (builder 16 | .write_int(character.id) 17 | .write_string_rightpad(character.name, '\0', 13) 18 | .write(character.gender) 19 | .write(character.skin_color) 20 | .write_int(character.face) 21 | .write_int(character.hair) 22 | ) 23 | for i in xrange(3): # Pets - ignore 24 | builder.write_long(0) 25 | (builder 26 | .write(character.level) 27 | .write_short(character.job) 28 | .write_short(character.str) 29 | .write_short(character.dex) 30 | .write_short(character.int) 31 | .write_short(character.luk) 32 | .write_short(character.hp) 33 | .write_short(character.maxhp) 34 | .write_short(character.mp) 35 | .write_short(character.maxmp) 36 | .write_short(character.ap) 37 | .write_short(character.sp) 38 | .write_int(character.exp) 39 | .write_short(character.fame) 40 | .write_int(character.gachaexp) 41 | .write_int(character.map) 42 | .write(character.spawn_point) 43 | .write_int(0) 44 | ) 45 | return 46 | -------------------------------------------------------------------------------- /pythonstory/common/packetreader.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | 4 | class PacketReader(object): 5 | 6 | def process(self, arr): 7 | data = bytes(bytearray(arr)) 8 | self.data = data[2:] 9 | self.opcode = struct.unpack_from(' packetlength: 35 | self.dataReceived(data[:4+packetlength]) 36 | self.dataReceived(data[packetlength+4:]) 37 | 38 | def packet_received(self, packet): 39 | packet = self.reader.process(self.decoder.decode(packet)) 40 | self.processor.handle_packet(packet, self) 41 | 42 | def connectionMade(self): 43 | print "New connection {} on {} ".format( 44 | self.transport.getPeer().host, 45 | self.__class__.__name__ 46 | ) 47 | self.factory.connections.append(self) 48 | self.send_connect() 49 | self.pingcall.start(self.PING_TIME, now=True) 50 | 51 | def connectionLost(self, reason): 52 | print "Closed connection {} on {}".format( 53 | self.transport.getPeer().host, 54 | self.__class__.__name__ 55 | ) 56 | self.pingcall.stop() 57 | self.factory.connections.remove(self) 58 | 59 | def send_connect(self): 60 | data = struct.pack( 61 | ' 100 and slot != 111: 23 | slot -= 100 24 | if slot in equips: 25 | masked_equips[slot] = equips[slot] 26 | equips[slot] = equip.itemid 27 | elif slot in equips: 28 | masked_equips[slot] = equip.itemid 29 | 30 | for k, v in equips.iteritems(): 31 | (builder 32 | .write(k) 33 | .write_int(v) 34 | ) 35 | builder.write(0xFF) 36 | for k, v in masked_equips.iteritems(): 37 | (builder 38 | .write(k) 39 | .write_int(v) 40 | ) 41 | (builder 42 | .write(0xFF) 43 | .write_int(0) # Cash weapon 44 | ) 45 | for _ in xrange(3): # Pets, deal with later 46 | builder.write_int(0) 47 | 48 | if not viewall: 49 | builder.write(0) 50 | 51 | (builder 52 | .write(1) 53 | .write_int(character.rank) 54 | .write_int(character.rank_move) 55 | .write_int(character.job_rank) 56 | .write_int(character.job_rank_move) 57 | ) 58 | 59 | return 60 | -------------------------------------------------------------------------------- /pythonstory/world/packets.py: -------------------------------------------------------------------------------- 1 | from ..common import sendopcodes 2 | from ..common.decorators import packet 3 | from ..channel.models import Keymap 4 | 5 | from . import packethelpers 6 | from ..common.packethelpers import maple_time 7 | 8 | import random 9 | import time 10 | 11 | 12 | @packet(sendopcodes.LOGIN_STATUS) 13 | def auth_success(builder, client): 14 | (builder 15 | .write_int(0) 16 | .write_short(0) 17 | .write_int(client.account.id) 18 | .write(client.account.gender) 19 | .write(client.account.gm > 0) 20 | ) 21 | temp = client.account.gm * 32 22 | (builder 23 | .write(0x80 if temp > 0x80 else temp) # wtf admin towrite byte 24 | .write(client.account.gm > 0) 25 | .write_string(client.account.name) 26 | .write(0) 27 | .write(0) 28 | .write_long(0) 29 | .write_long(0) 30 | .write_int(0) 31 | .write_short(2) 32 | ) 33 | return builder.get_packet() 34 | 35 | 36 | @packet(sendopcodes.LOGIN_STATUS) 37 | def auth_failed(builder, reason): 38 | (builder 39 | .write(reason) 40 | .write(0) 41 | .write_int(0) 42 | ) 43 | return builder.get_packet() 44 | 45 | 46 | @packet(sendopcodes.SERVERLIST) 47 | def serverlist(builder, world): 48 | (builder 49 | .write(world.key) 50 | .write_string(world.name) 51 | .write(world.flag) 52 | .write_string(world.eventmsg) 53 | .write(100) # Rate modifier? Ask moople 54 | .write(0) # event xp * 2.6 ? 55 | .write(100) # rate modifier? ask moople 56 | .write(0) # drop rate * 2.6 57 | .write(0) 58 | .write(len(world.channels)) 59 | ) 60 | for channel in world.channels: 61 | (builder 62 | .write_string('{}-{}'.format(world.name, channel.key)) 63 | .write_int(1) # server load 64 | .write(1) 65 | .write_short(channel.key - 1) 66 | ) 67 | builder.write_short(0) 68 | return builder.get_packet() 69 | 70 | 71 | @packet(sendopcodes.SERVERLIST) 72 | def end_of_serverlist(builder): 73 | builder.write(0xFF) 74 | return builder.get_packet() 75 | 76 | 77 | @packet(sendopcodes.LAST_CONNECTED_WORLD) 78 | def select_world(builder, worldid): 79 | builder.write_int(worldid) 80 | return builder.get_packet() 81 | 82 | 83 | @packet(sendopcodes.RECOMMENDED_WORLD_MESSAGE) 84 | def recommended_worlds(builder, worlds): 85 | builder.write(len(worlds)) 86 | for w in worlds: 87 | (builder 88 | .write_int(w.key) 89 | .write_string(w.recommended) 90 | ) 91 | return builder.get_packet() 92 | 93 | 94 | @packet(sendopcodes.SERVERSTATUS) 95 | def server_load(builder, load): 96 | builder.write_short(load) 97 | return builder.get_packet() 98 | 99 | 100 | @packet(sendopcodes.CHARLIST) 101 | def charlist(builder, account): 102 | builder.write(0) 103 | characters = account.characters 104 | builder.write(characters.count()) 105 | for character in characters: 106 | packethelpers.add_character_entry(builder, character) 107 | (builder 108 | .write(2) # No pic 109 | .write_int(account.character_slots) 110 | ) 111 | return builder.get_packet() 112 | 113 | 114 | @packet(sendopcodes.CHAR_NAME_RESPONSE) 115 | def char_name_response(builder, name, available): 116 | (builder 117 | .write_string(name) 118 | .write(not available) 119 | ) 120 | return builder.get_packet() 121 | 122 | 123 | @packet(sendopcodes.ADD_NEW_CHAR_ENTRY) 124 | def create_character(builder, character): 125 | builder.write(0) 126 | packethelpers.add_character_entry(builder, character) 127 | return builder.get_packet() 128 | 129 | 130 | @packet(sendopcodes.SERVER_IP) 131 | def serverip(builder, ip, port, char_id): 132 | (builder 133 | .write_short(0) 134 | .write_array((int(c) for c in ip.split('.'))) 135 | .write_short(port) 136 | .write_int(char_id) 137 | .write_array([0] * 5) 138 | ) 139 | return builder.get_packet() 140 | -------------------------------------------------------------------------------- /pythonstory/world/processor.py: -------------------------------------------------------------------------------- 1 | from ..common import processor, recvopcodes 2 | from ..common import handlers as commonhandlers 3 | from . import handlers as worldhandlers 4 | 5 | 6 | class WorldPacketProcessor(processor.BasePacketProcessor): 7 | ignored_opcodes = ( 8 | recvopcodes.CLIENT_STARTED, 9 | recvopcodes.CLIENT_START_ERROR, 10 | recvopcodes.PLAYER_DC, 11 | recvopcodes.PLAYER_UPDATE 12 | ) 13 | handlers = { 14 | recvopcodes.PONG: commonhandlers.pong, 15 | recvopcodes.LOGIN_PASSWORD: worldhandlers.login, 16 | recvopcodes.SERVERLIST_REQUEST: worldhandlers.serverlist, 17 | recvopcodes.SERVERLIST_REREQUEST: worldhandlers.serverlist, 18 | recvopcodes.SERVERSTATUS_REQUEST: worldhandlers.server_status_load, 19 | recvopcodes.CHARLIST_REQUEST: worldhandlers.charlist, 20 | recvopcodes.CHECK_CHAR_NAME: worldhandlers.check_charname, 21 | recvopcodes.CREATE_CHAR: worldhandlers.create_character, 22 | recvopcodes.CHAR_SELECTED: worldhandlers.character_selected, 23 | } 24 | -------------------------------------------------------------------------------- /pythonstory/world/protocol.py: -------------------------------------------------------------------------------- 1 | from ..common.protocol import MapleProtocol 2 | from .processor import WorldPacketProcessor 3 | 4 | 5 | class WorldProtocol(MapleProtocol): 6 | processor = WorldPacketProcessor 7 | 8 | def __init__(self, *args, **kwargs): 9 | self.channel = -1 10 | self.account = None 11 | super(WorldProtocol, self).__init__(*args, **kwargs) 12 | -------------------------------------------------------------------------------- /pythonstory/world/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martolini/pythonstory/121701004fd7f2e326c4969e8400958d39c74f76/pythonstory/world/tests/__init__.py -------------------------------------------------------------------------------- /pythonstory/world/tests/test_packets.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from ..packets import charlist 3 | from ..models import Account 4 | 5 | 6 | class PacketTest(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.account = Account.create(id=999, name='test', password='test') 10 | 11 | def test_charlist(self): 12 | self.assertEqual(type(self.account), Account) 13 | packet = charlist(self.account) 14 | self.assertEqual(len(packet), 9) 15 | 16 | def tearDown(self): 17 | self.account.delete_instance() 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | enum34==1.0.4 2 | nose==1.3.7 3 | peewee==2.6.3 4 | pycrypto==2.6.1 5 | Twisted==15.4.0 6 | zope.interface==4.1.2 7 | -------------------------------------------------------------------------------- /staticdb.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martolini/pythonstory/121701004fd7f2e326c4969e8400958d39c74f76/staticdb.sqlite --------------------------------------------------------------------------------