diff --git a/luserver/auth.py b/luserver/auth.py index 08eb529..6732a67 100644 --- a/luserver/auth.py +++ b/luserver/auth.py @@ -31,7 +31,7 @@ class LoginReturnCode: AccountNotActivated = 14 class AuthServer(server.Server): - PEER_TYPE = AuthServerMsg.__int__() + PEER_TYPE = AuthServerMsg.header() def __init__(self, host, max_connections, db_conn): super().__init__((host, 1001), max_connections, db_conn) diff --git a/luserver/bitstream.py b/luserver/bitstream.py index b8e9762..530983b 100644 --- a/luserver/bitstream.py +++ b/luserver/bitstream.py @@ -3,7 +3,7 @@ from .messages import Message def write_header(self, subheader): self.write(c_ubyte(Message.LUPacket)) - self.write(c_ushort(type(subheader).__int__())) + self.write(c_ushort(type(subheader).header())) self.write(c_uint(subheader)) self.write(c_ubyte(0x00)) diff --git a/luserver/components/char.py b/luserver/components/char.py index 21b1e7d..6b0f283 100644 --- a/luserver/components/char.py +++ b/luserver/components/char.py @@ -5,6 +5,7 @@ from persistent.list import PersistentList from persistent.mapping import PersistentMapping from ..bitstream import BitStream, c_bit, c_bool, c_float, c_int, c_int64, c_ubyte, c_uint, c_uint64, c_ushort +from ..ldf import LDF, LDFDataType from ..messages import WorldClientMsg from ..world import World from ..math.quaternion import Quaternion @@ -287,7 +288,7 @@ class CharacterComponent(Component): loot.append(lot) return loot - async def transfer_to_world(self, world, respawn_point_name=None): + async def transfer_to_world(self, world, respawn_point_name=None, include_self=False): if respawn_point_name is not None: for obj in self.object._v_server.db.world_data[world[0]].objects.values(): if obj.lot == 4945 and (not hasattr(obj, "respawn_name") or respawn_point_name == "" or obj.respawn_name == respawn_point_name): # respawn point lot @@ -299,9 +300,9 @@ class CharacterComponent(Component): self.object.physics.rotation.update(self.object._v_server.db.world_data[world[0]].spawnpoint[1]) self.object.physics.attr_changed("position") self.object.physics.attr_changed("rotation") - self.object._v_server.commit() + self.object._v_server.commit() - server_address = await self.object._v_server.address_for_world(world) + server_address = await self.object._v_server.address_for_world(world, include_self) log.info("Sending redirect to world %s", server_address) redirect = BitStream() redirect.write_header(WorldClientMsg.Redirect) @@ -310,6 +311,15 @@ class CharacterComponent(Component): redirect.write(c_bool(False)) self.object._v_server.send(redirect, self.address) + async def transfer_to_last_non_instance(self, position=None, rotation=None): + if position is not None: + self.object.physics.position.update(position) + self.object.physics.attr_changed("position") + if rotation is not None: + self.object.physics.rotation.update(rotation) + self.object.physics.attr_changed("rotation") + await self.transfer_to_world(((self.world[0] // 100)*100, self.world[1], self.world[2])) + def add_mission(self, mission_id): mission_progress = MissionProgress(mission_id, self.object._v_server.db.missions[mission_id]) self.missions.append(mission_progress) @@ -439,7 +449,6 @@ class CharacterComponent(Component): if task.type == TaskType.Flag and flag_id in task.target: mission.increment_task(task, self.object) - def player_loaded(self, address, player_id:c_int64=None): pass @@ -452,7 +461,7 @@ class CharacterComponent(Component): def set_jet_pack_mode(self, address, bypass_checks:c_bit=True, hover:c_bit=False, enable:c_bit=False, effect_id:c_uint=-1, air_speed:c_float=10, max_air_speed:c_float=15, vertical_velocity:c_float=1, warning_effect_id:c_uint=-1): pass - def display_tooltip(self, address, do_or_die:c_bit=False, no_repeat:c_bit=False, no_revive:c_bit=False, is_property_tooltip:c_bit=False, show:c_bit=None, translate:c_bit=False, time:c_int=None, id:"wstr"=None, localize_params:"ldf"=None, str_image_name:"wstr"=None, str_text:"wstr"=None): + def display_tooltip(self, address, do_or_die:c_bit=False, no_repeat:c_bit=False, no_revive:c_bit=False, is_property_tooltip:c_bit=False, show:c_bit=None, translate:c_bit=False, time:c_int=None, id:"wstr"=None, localize_params:LDF=None, str_image_name:"wstr"=None, str_text:"wstr"=None): pass def use_non_equipment_item(self, address, item_to_use:c_int64=None): @@ -562,18 +571,18 @@ class CharacterComponent(Component): def get_models_on_property(self, address, models:(c_uint, c_int64, c_int64)=None): pass - def match_request(self, address, activator:c_int64=None, player_choices:"ldf"=None, type:c_int=None, value:c_int=None): + def match_request(self, address, activator:c_int64=None, player_choices:LDF=None, type:c_int=None, value:c_int=None): # todo: how does the server know which matchmaking activity the client wants? self.object._v_server.send_game_message(self.match_response, response=0, address=address) if type == MatchRequestType.Join:# and value == MatchRequestValue.Join: - update_data = {} - update_data["time"] = c_float, 60 + update_data = LDF() + update_data.ldf_set("time", LDFDataType.FLOAT, 60.0) self.object._v_server.send_game_message(self.match_update, data=update_data, type=MatchUpdateType.Time, address=address) def match_response(self, address, response:c_int=None): pass - def match_update(self, address, data:"ldf"=None, type:c_int=None): + def match_update(self, address, data:LDF=None, type:c_int=None): pass def used_information_plaque(self, address, plaque_object_id:c_int64=None): diff --git a/luserver/components/collectible.py b/luserver/components/collectible.py index 3cc7b37..d2aa112 100644 --- a/luserver/components/collectible.py +++ b/luserver/components/collectible.py @@ -3,8 +3,12 @@ from .component import Component from .mission import MissionState, TaskType class CollectibleComponent(Component): + def __init__(self, obj, set_vars, comp_id): + super().__init__(obj, set_vars, comp_id) + self.collectible_id = set_vars.get("collectible_id", 0) + def serialize(self, out, is_creation): - out.write(c_ushort(0)) # todo + out.write(c_ushort(self.collectible_id)) def has_been_collected(self, address, player_id:c_int64=None): player = self.object._v_server.game_objects[player_id] @@ -13,4 +17,5 @@ class CollectibleComponent(Component): if mission.state == MissionState.Active: for task in mission.tasks: if task.type == TaskType.Collect and task.target == self.object.lot: - mission.increment_task(task, player) + coll_id = self.collectible_id+(self.object._v_server.world_id[0]<<8) + mission.increment_task(task, player, increment=coll_id) diff --git a/luserver/components/comp108.py b/luserver/components/comp108.py index 7aaf9ae..93d4ff3 100644 --- a/luserver/components/comp108.py +++ b/luserver/components/comp108.py @@ -1,5 +1,6 @@ import asyncio from ..bitstream import c_bit, c_float, c_int, c_int64 +from ..ldf import LDF from .component import Component class Comp108Component(Component): @@ -25,7 +26,7 @@ class Comp108Component(Component): assert multi_interact_id is None self.driver_id = player.object_id player.char.vehicle_id = self.object.object_id - self.object._v_server.send_game_message(player.char.display_tooltip, show=True, time=1000, id="", localize_params={}, str_image_name="", str_text="Use /dismount to dismount.", address=player.char.address) + self.object._v_server.send_game_message(player.char.display_tooltip, show=True, time=1000, id="", localize_params=LDF(), str_image_name="", str_text="Use /dismount to dismount.", address=player.char.address) def request_die(self, address, unknown_bool:c_bit=None, death_type:"wstr"=None, direction_relative_angle_xz:c_float=None, direction_relative_angle_y:c_float=None, direction_relative_force:c_float=None, kill_type:c_int=0, killer_id:c_int64=None, loot_owner_id:c_int64=None): #self.object.destructible.deal_damage(10000, self) # die permanently on crash diff --git a/luserver/components/inventory.py b/luserver/components/inventory.py index 1e8b6db..7b26ca5 100644 --- a/luserver/components/inventory.py +++ b/luserver/components/inventory.py @@ -36,6 +36,7 @@ from persistent import Persistent from persistent.list import PersistentList from ..bitstream import c_bit, c_int, c_int64, c_uint +from ..ldf import LDF, LDFDataType from ..math.vector import Vector3 from .component import Component from .mission import MissionState, TaskType @@ -196,9 +197,9 @@ class InventoryComponent(Component): if hasattr(self.object, "char"): if notify_client: - extra_info = {} + extra_info = LDF() if hasattr(stack, "module_lots"): - extra_info["assemblyPartLOTs"] = str, [(c_int, i) for i in stack.module_lots] + extra_info.ldf_set("assemblyPartLOTs", LDFDataType.STRING, [(LDFDataType.INT32, i) for i in stack.module_lots]) self.object._v_server.send_game_message(self.add_item_to_inventory_client_sync, bound=True, bound_on_equip=True, bound_on_pickup=True, loot_type_source=source_type, extra_info=extra_info, object_template=stack.lot, inv_type=inventory_type, amount=1, new_obj_id=stack.object_id, flying_loot_pos=Vector3.zero, show_flying_loot=show_flying_loot, slot_id=index, address=self.object.char.address) @@ -212,7 +213,7 @@ class InventoryComponent(Component): return stack - def add_item_to_inventory_client_sync(self, address, bound:c_bit=False, bound_on_equip:c_bit=False, bound_on_pickup:c_bit=False, loot_type_source:c_int=0, extra_info:"ldf"=None, object_template:c_int=None, subkey:c_int64=0, inv_type:c_int=0, amount:c_uint=1, item_total:c_uint=0, new_obj_id:c_int64=None, flying_loot_pos:Vector3=None, show_flying_loot:c_bit=True, slot_id:c_int=None): + def add_item_to_inventory_client_sync(self, address, bound:c_bit=False, bound_on_equip:c_bit=False, bound_on_pickup:c_bit=False, loot_type_source:c_int=0, extra_info:LDF=None, object_template:c_int=None, subkey:c_int64=0, inv_type:c_int=0, amount:c_uint=1, item_total:c_uint=0, new_obj_id:c_int64=None, flying_loot_pos:Vector3=None, show_flying_loot:c_bit=True, slot_id:c_int=None): pass def remove_item_from_inv(self, inventory_type, item=None, object_id=0, lot=0, amount=1): @@ -220,9 +221,9 @@ class InventoryComponent(Component): object_id = item.object_id if hasattr(self.object, "char"): - self.object._v_server.send_game_message(self.remove_item_from_inventory, inventory_type=inventory_type, extra_info={}, force_deletion=True, object_id=object_id, object_template=lot, stack_count=amount, address=self.object.char.address) + self.object._v_server.send_game_message(self.remove_item_from_inventory, inventory_type=inventory_type, extra_info=LDF, force_deletion=True, object_id=object_id, object_template=lot, stack_count=amount, address=self.object.char.address) - def remove_item_from_inventory(self, address, confirmed:c_bit=True, delete_item:c_bit=True, out_success:c_bit=False, inventory_type:c_int=0, loot_type_source:c_int=0, extra_info:"ldf"=None, force_deletion:c_bit=False, loot_type_source_id:c_int64=0, object_id:c_int64=0, object_template:c_int=0, requesting_object_id:c_int64=0, stack_count:c_uint=1, stack_remaining:c_uint=0, subkey:c_int64=0, trade_id:c_int64=0): + def remove_item_from_inventory(self, address, confirmed:c_bit=True, delete_item:c_bit=True, out_success:c_bit=False, inventory_type:c_int=0, loot_type_source:c_int=0, extra_info:LDF=None, force_deletion:c_bit=False, loot_type_source_id:c_int64=0, object_id:c_int64=0, object_template:c_int=0, requesting_object_id:c_int64=0, stack_count:c_uint=1, stack_remaining:c_uint=0, subkey:c_int64=0, trade_id:c_int64=0): if confirmed: assert delete_item assert not out_success diff --git a/luserver/components/mission.py b/luserver/components/mission.py index 4a87bb2..06b77e9 100644 --- a/luserver/components/mission.py +++ b/luserver/components/mission.py @@ -57,6 +57,8 @@ class MissionTask(Persistent): self.target = target self.value = 0 self.target_value = target_value + if task_type == TaskType.Collect: + parameter = set() # used for collectibles self.parameter = parameter class MissionProgress(Persistent): @@ -84,9 +86,16 @@ class MissionProgress(Persistent): if not self.is_mission and not check_prereqs(self.id, player): return - task.value = min(task.value+increment, task.target_value) task_index = self.tasks.index(task) - player._v_server.send_game_message(player.char.notify_mission_task, self.id, task_mask=1<<(task_index+1), updates=[task.value], address=player.char.address) + + if task.type == TaskType.Collect: + task.parameter.add(increment) + task.value = len(task.parameter) + update = increment + else: + task.value = min(task.value+increment, task.target_value) + update = task.value + player._v_server.send_game_message(player.char.notify_mission_task, self.id, task_mask=1<<(task_index+1), updates=[update], address=player.char.address) if not self.is_mission: for task in self.tasks: if task.value < task.target_value: diff --git a/luserver/components/moving_platform.py b/luserver/components/moving_platform.py index 135144e..e46622e 100644 --- a/luserver/components/moving_platform.py +++ b/luserver/components/moving_platform.py @@ -8,6 +8,7 @@ from .component import Component class MovingPlatformComponent(Component): def __init__(self, obj, set_vars, comp_id): super().__init__(obj, set_vars, comp_id) + self.object.moving_platform = self self._flags["moving_platform_unknown"] = "moving_platform_flag" self._flags["target_position"] = "moving_platform_flag" self.target_position = Vector3() diff --git a/luserver/components/rebuild.py b/luserver/components/rebuild.py index eccc6c6..c8291b1 100644 --- a/luserver/components/rebuild.py +++ b/luserver/components/rebuild.py @@ -123,6 +123,10 @@ class RebuildComponent(ScriptedActivityComponent): # drop rewards self.object.stats.drop_rewards(*self.completion_rewards, player) + # if this is a moving platform, set the waypoint + if hasattr(self, "moving_platform"): + self.moving_platform.update_waypoint() + # update missions that have completing this rebuild as requirement for mission in player.char.missions: if mission.state == MissionState.Active: diff --git a/luserver/components/script.py b/luserver/components/script.py index 2f6a3cd..0fa193a 100644 --- a/luserver/components/script.py +++ b/luserver/components/script.py @@ -1,3 +1,4 @@ +from .. ldf import LDF from ..bitstream import c_bit, c_int, c_int64 from .component import Component @@ -9,11 +10,15 @@ class ScriptComponent(Component): if "script_vars" in set_vars: self.script_vars = set_vars["script_vars"] + self.script_network_vars = LDF() + def serialize(self, out, is_creation): if is_creation: - out.write(c_bit(False)) + out.write(c_bit(self.script_network_vars)) + if self.script_network_vars: + out.write(self.script_network_vars.to_bitstream()) - def script_network_var_update(self, address, script_vars:"ldf"=None): + def script_network_var_update(self, address, script_vars:LDF=None): pass def notify_client_object(self, address, name:"wstr"=None, param1:c_int=None, param2:c_int=None, param_obj:c_int64=None, param_str:"str"=None): diff --git a/luserver/components/scripted_activity.py b/luserver/components/scripted_activity.py index 721bc35..30d1b4e 100644 --- a/luserver/components/scripted_activity.py +++ b/luserver/components/scripted_activity.py @@ -21,6 +21,9 @@ class ScriptedActivityComponent(Component): out.write(c_float(0)) self.activity_flag = False + def activity_start(self, address): + pass + def message_box_respond(self, address, button:c_int=None, identifier:"wstr"=None, user_data:"wstr"=None): if identifier == "LobbyReady" and button == 1: player = self.object._v_server.accounts[address].characters.selected() diff --git a/luserver/game_object.py b/luserver/game_object.py index a2fe18a..16154c7 100644 --- a/luserver/game_object.py +++ b/luserver/game_object.py @@ -4,7 +4,7 @@ from collections import OrderedDict from persistent import Persistent -from . import ldf +from .ldf import LDF from .bitstream import BitStream, c_bit, c_float, c_int, c_int64, c_ubyte, c_uint, c_ushort from .components.ai import BaseCombatAIComponent from .components.bouncer import BouncerComponent @@ -89,7 +89,7 @@ class GameObject: self.lot = lot self.object_id = object_id self.name = "" - self.config = set_vars.get("config", {}) + self.config = set_vars.get("config", LDF()) self.spawner_object = None self.spawner_waypoint_index = 0 self.scale = set_vars.get("scale", 1) @@ -170,7 +170,7 @@ class GameObject: out.write(bytes(4)) # time since created on server? out.write(c_bit(self.config)) if self.config: - out.write(ldf.to_ldf(self.config, ldf_type="binary")) + out.write(self.config.to_bitstream()) out.write(c_bit(hasattr(self, "trigger"))) out.write(c_bit(self.spawner_object is not None)) if self.spawner_object is not None: diff --git a/luserver/ldf.py b/luserver/ldf.py index c9bdfcc..e56c417 100644 --- a/luserver/ldf.py +++ b/luserver/ldf.py @@ -1,55 +1,145 @@ +import enum + from .bitstream import BitStream, c_bool, c_float, c_int, c_int64, c_ubyte, c_uint -DATA_TYPE = {} -DATA_TYPE[str] = 0 -DATA_TYPE[c_int] = 1 -DATA_TYPE[c_float] = 3 -DATA_TYPE[c_bool] = 7 -DATA_TYPE[c_int64] = 9 # or 8? -DATA_TYPE[bytes] = 13 # let's just use bytes for 13 -DATA_TYPE[0] = str -DATA_TYPE[1] = int # signed int -DATA_TYPE[3] = float -DATA_TYPE[5] = int # unsigned int -DATA_TYPE[7] = lambda x: bool(int(x)) -DATA_TYPE[9] = int -DATA_TYPE[13] = lambda x: x.encode() # see above +class LDFDataType(enum.Enum): + STRING = 0 + INT32 = 1 + FLOAT = 3 + UINT32 = 5 + BOOLEAN = 7 + INT64_8 = 8 + INT64_9 = 9 + BYTES = 13 -def _value_to_ldf_text(type_, value): - if isinstance(value, (list, tuple)): - str_value = "+".join(_value_to_ldf_text(typ, val) for typ, val in value) - value = "" - else: - str_value = str(value) - return "%i:%s" % (DATA_TYPE[type_], str_value) +class LDF(dict): + def __init__(self, source=None): + super().__init__() + if source is not None: + if isinstance(source, str): + self.from_str(source) + elif isinstance(source, BitStream): + self.from_bitstream(source) -def to_ldf(obj, ldf_type): - if ldf_type == "text": - if isinstance(obj, dict): - output = "\n".join("%s=%s" % (key, _value_to_ldf_text(*value)) for key, value in obj.items()) + def __getitem__(self, key): + return self.ldf_get(key)[1] + + def __setitem__(self, key, value): + return self.ldf_set(key, self.ldf_get(key)[0], value) + + def ldf_get(self, key): + return super().__getitem__(key) + + def ldf_set(self, key, data_type, value): + if not isinstance(data_type, LDFDataType): + raise TypeError + if data_type == LDFDataType.STRING: + if not isinstance(value, (str, list, tuple)): + raise TypeError + elif data_type in (LDFDataType.INT32, LDFDataType.UINT32, LDFDataType.INT64_8, LDFDataType.INT64_9): + if not isinstance(value, int): + raise TypeError + if data_type == LDFDataType.UINT32: + if value < 0: + raise ValueError + elif data_type == LDFDataType.FLOAT: + if not isinstance(value, float): + raise TypeError + elif data_type == LDFDataType.BOOLEAN: + if not isinstance(value, bool): + raise TypeError + elif data_type == LDFDataType.BYTES: + if not isinstance(value, bytes): + raise TypeError + return super().__setitem__(key, (data_type, value)) + + def from_str(self, source): + items = source.split("\x0a") + for item in items: + key, type_value = item.split("=", maxsplit=1) + data_type, value = self.from_str_type(type_value) + self.ldf_set(key, data_type, value) + + def to_str(self): + return "\n".join("%s=%s" % (key, self.to_str_type(*value)) for key, value in super().items()) + + @staticmethod + def from_str_type(type_value): + data_type_id, value = type_value.split(":", maxsplit=1) + data_type = LDFDataType(int(data_type_id)) + if data_type == LDFDataType.STRING: + value = value + elif data_type in (LDFDataType.INT32, LDFDataType.UINT32, LDFDataType.INT64_8, LDFDataType.INT64_9): + value = int(value) + elif data_type == LDFDataType.FLOAT: + value = float(value) + elif data_type == LDFDataType.BOOLEAN: + value = bool(int(value)) + elif data_type == LDFDataType.BYTES: + value = value.encode() + return data_type, value + + @staticmethod + def to_str_type(data_type, value): + if isinstance(value, (list, tuple)): + str_value = "+".join(LDF.to_str_type(data_type, val) for data_type, val in value) else: - output = _value_to_ldf_text(*obj) + if data_type == LDFDataType.STRING: + str_value = value + elif data_type in (LDFDataType.INT32, LDFDataType.FLOAT, LDFDataType.UINT32, LDFDataType.INT64_8, LDFDataType.INT64_9): + str_value = str(value) + elif data_type == LDFDataType.BOOLEAN: + str_value = str(int(value)) + elif data_type == LDFDataType.BYTES: + str_value = value.decode() + return "%i:%s" % (data_type.value, str_value) - elif ldf_type == "binary": - if not isinstance(obj, dict): - raise NotImplementedError + def from_bitstream(self, source): + for _ in range(source.read(c_uint)): + encoded_key = source.read(bytes, length=source.read(c_ubyte)) + key = encoded_key.decode("utf-16-le") + data_type = LDFDataType(source.read(c_ubyte)) + if data_type == LDFDataType.STRING: + value = source.read(str, length_type=c_uint) + elif data_type == LDFDataType.INT32: + value = source.read(c_int) + elif data_type == LDFDataType.FLOAT: + value = source.read(c_float) + elif data_type == LDFDataType.UINT32: + value = source.read(c_uint) + elif data_type == LDFDataType.BOOLEAN: + value = source.read(c_bool) + elif data_type in (LDFDataType.INT64_8, LDFDataType.INT64_9): + value = source.read(c_int64) + elif data_type == LDFDataType.BYTES: + value = source.read(bytes, length=source.read(c_uint)) + self.ldf_set(key, data_type, value) + + def to_bitstream(self): uncompressed = BitStream() - uncompressed.write(c_uint(len(obj))) - for key, value in obj.items(): - value_type, value_value = value # meh this isn't exactly descriptive - + uncompressed.write(c_uint(len(self))) + for key, value in super().items(): + data_type, value = value # can't use normal variable string writing because this writes the length of the encoded instead of the original (include option for this behavior?) encoded_key = key.encode("utf-16-le") uncompressed.write(c_ubyte(len(encoded_key))) uncompressed.write(encoded_key) - uncompressed.write(c_ubyte(DATA_TYPE[value_type])) - if value_type == str: - uncompressed.write(value_value, length_type=c_uint) - - else: - if value_type == bytes: - uncompressed.write(c_uint(len(value_value))) - uncompressed.write(value_type(value_value)) + uncompressed.write(c_ubyte(data_type.value)) + if data_type == LDFDataType.STRING: + uncompressed.write(value, length_type=c_uint) + elif data_type == LDFDataType.INT32: + uncompressed.write(c_int(value)) + elif data_type == LDFDataType.FLOAT: + uncompressed.write(c_float(value)) + elif data_type == LDFDataType.UINT32: + uncompressed.write(c_uint(value)) + elif data_type == LDFDataType.BOOLEAN: + uncompressed.write(c_bool(value)) + elif data_type in (LDFDataType.INT64_8, LDFDataType.INT64_9): + uncompressed.write(c_int64(value)) + elif data_type == LDFDataType.BYTES: + uncompressed.write(c_uint(len(value))) + uncompressed.write(value) output = BitStream() is_compressed = False @@ -59,43 +149,4 @@ def to_ldf(obj, ldf_type): raise NotImplementedError output.write(c_bool(is_compressed)) output.write(uncompressed) - - return output - -def from_ldf_type_value(type_value): # in its own function for use in luz path spawners where the structure is different - data_type_id, value = type_value.split(":", maxsplit=1) - data_type_id = int(data_type_id) - return DATA_TYPE[data_type_id](value) - -def from_ldf(ldf): - ldf_dict = {} - if isinstance(ldf, str): - items = ldf.split("\x0a") - for item in items: - key, type_value = item.split("=", maxsplit=1) - value = from_ldf_type_value(type_value) - ldf_dict[key] = value - elif isinstance(ldf, BitStream): - for _ in range(ldf.read(c_uint)): - encoded_key = ldf.read(bytes, length=ldf.read(c_ubyte)) - key = encoded_key.decode("utf-16-le") - data_type_id = ldf.read(c_ubyte) - if data_type_id == 0: - value = ldf.read(str, length_type=c_uint) - elif data_type_id == 1: - value = ldf.read(c_int) - elif data_type_id == 5: - value = ldf.read(c_uint) - elif data_type_id == 7: - value = ldf.read(c_bool) - elif data_type_id in (8, 9): - value = ldf.read(c_int64) - elif data_type_id == 13: - value = ldf.read(bytes, length=ldf.read(c_uint)) - else: - raise NotImplementedError(key, data_type_id) - ldf_dict[key] = value - else: - raise NotImplementedError - - return ldf_dict + return output diff --git a/luserver/messages.py b/luserver/messages.py index 75458df..529564a 100644 --- a/luserver/messages.py +++ b/luserver/messages.py @@ -4,11 +4,9 @@ from pyraknet.messages import Message Message.LUPacket = 0x53 -# for the enums below, __int__() returns the corresponding header - class GeneralMsg(IntEnum): @staticmethod - def __int__(): + def header(): return 0x00 Handshake = 0x00 @@ -17,14 +15,14 @@ class GeneralMsg(IntEnum): class AuthServerMsg(IntEnum): @staticmethod - def __int__(): + def header(): return 0x01 LoginRequest = 0x00 class SocialMsg(IntEnum): @staticmethod - def __int__(): + def header(): return 0x02 GeneralChatMessage = 0x01 @@ -40,7 +38,7 @@ class SocialMsg(IntEnum): class WorldServerMsg(IntEnum): @staticmethod - def __int__(): + def header(): return 0x04 SessionInfo = 0x01 @@ -58,7 +56,7 @@ class WorldServerMsg(IntEnum): class WorldClientMsg(IntEnum): @staticmethod - def __int__(): + def header(): return 0x05 LoginResponse = 0x00 @@ -90,7 +88,7 @@ msg_enum[0x05] = WorldClientMsg class GameMessage(Enum): Teleport = 19 DropClientLoot = 30 - Die = 37 + Die = 37 RequestDie = 38 PlayEmote = 41 PlayAnimation = 43 @@ -126,6 +124,7 @@ class GameMessage(Enum): BuyFromVendor = 373 SellToVendor = 374 SetInventorySize = 389 + ActivityStart = 407 VendorStatusUpdate = 417 ClientItemConsumed = 428 SetFlag = 471 diff --git a/luserver/modules/char.py b/luserver/modules/char.py index 00393df..a48c186 100644 --- a/luserver/modules/char.py +++ b/luserver/modules/char.py @@ -218,7 +218,7 @@ class CharHandling(ServerModule): selected_char.char.world = 1000, 0, 0 asyncio.ensure_future(selected_char.char.transfer_to_world(selected_char.char.world, respawn_point_name="")) else: - asyncio.ensure_future(selected_char.char.transfer_to_world(selected_char.char.world)) + asyncio.ensure_future(selected_char.char.transfer_to_world(selected_char.char.world, include_self=True)) def shirt_to_lot(self, color, style): # The LOTs for the shirts are for some reason in two batches of IDs diff --git a/luserver/modules/general.py b/luserver/modules/general.py index 68fe416..81c012d 100644 --- a/luserver/modules/general.py +++ b/luserver/modules/general.py @@ -4,10 +4,11 @@ For world server packet handling that is general enough not to be grouped in a s import logging import xml.etree.ElementTree as ET -from .. import ldf +from ..ldf import LDF, LDFDataType from ..bitstream import BitStream, c_bit, c_float, c_int, c_int64, c_uint, c_ushort from ..messages import WorldClientMsg, WorldServerMsg from ..world import World +from ..components.mission import TaskType from .module import ServerModule log = logging.getLogger(__name__) @@ -112,8 +113,8 @@ class GeneralHandling(ServerModule): for model in models: i = ET.SubElement(in_5, "i", l=str(model.lot), c=str(model.amount), id=str(model.object_id), s=str(player.inventory.models.index(model))) if hasattr(model, "module_lots"): - module_lots = [(c_int, i) for i in model.module_lots] - module_lots = ldf.to_ldf((str, module_lots), ldf_type="text") + module_lots = [(LDFDataType.INT32, i) for i in model.module_lots] + module_lots = LDF().to_str_type(LDFDataType.STRING, module_lots) ET.SubElement(i, "x", ma=module_lots) flag = ET.SubElement(root, "flag") @@ -138,6 +139,9 @@ class GeneralHandling(ServerModule): ET.SubElement(m, "sv") else: ET.SubElement(m, "sv", v=str(task.value)) + if task.type == TaskType.Collect: + for collectible_id in task.parameter: + ET.SubElement(m, "sv", v=str(collectible_id)) elif mission.state == 8: ET.SubElement(done, "m", id=str(mission.id)) @@ -145,13 +149,13 @@ class GeneralHandling(ServerModule): xml = xml.dom.minidom.parseString((ET.tostring(root, encoding="unicode"))) #log.debug(xml.toprettyxml(indent=" ")) - chd_ldf = {} - chd_ldf["objid"] = c_int64, player.object_id - chd_ldf["template"] = c_int, 1 - chd_ldf["name"] = str, player.name - chd_ldf["xmlData"] = bytes, ET.tostring(root) + chd_ldf = LDF() + chd_ldf.ldf_set("objid", LDFDataType.INT64_9, player.object_id) + chd_ldf.ldf_set("template", LDFDataType.INT32, 1) + chd_ldf.ldf_set("name", LDFDataType.STRING, player.name) + chd_ldf.ldf_set("xmlData", LDFDataType.BYTES, ET.tostring(root)) - encoded_ldf = ldf.to_ldf(chd_ldf, ldf_type="binary") + encoded_ldf = chd_ldf.to_bitstream() chardata.write(encoded_ldf) self.server.send(chardata, address) diff --git a/luserver/modules/physics.py b/luserver/modules/physics.py index ee2632b..9e584c9 100644 --- a/luserver/modules/physics.py +++ b/luserver/modules/physics.py @@ -1,6 +1,6 @@ import logging -from ..bitstream import c_float, c_int +from ..ldf import LDF, LDFDataType from ..math.vector import Vector3 from .module import ServerModule @@ -25,7 +25,7 @@ class AABB: # axis aligned bounding box def __init__(self, obj): if obj.lot in (10042, 14510, 16506): if obj.primitive_model_type != 1: - log.warn("Primitive model type not 1 %s", obj) + log.warning("Primitive model type not 1 %s", obj) rel_min = MODEL_DIMENSIONS[obj.lot][0] * obj.primitive_model_scale rel_max = MODEL_DIMENSIONS[obj.lot][1] * obj.primitive_model_scale else: @@ -113,11 +113,11 @@ class PhysicsHandling(ServerModule): self.debug_markers.append(self.server.spawn_object(obj.lot, set_vars=set_vars)) set_vars = {} set_vars["position"] = Vector3((aabb.min.x+aabb.max.x)/2, aabb.min.y, (aabb.min.z+aabb.max.z)/2) - config = {} - config["primitiveModelType"] = c_int, 1 - config["primitiveModelValueX"] = c_float, aabb.max.x-aabb.min.x - config["primitiveModelValueY"] = c_float, aabb.max.y-aabb.min.y - config["primitiveModelValueZ"] = c_float, aabb.max.z-aabb.min.z + config = LDF() + config.ldf_set("primitiveModelType", LDFDataType.INT32, 1) + config.ldf_set("primitiveModelValueX", LDFDataType.FLOAT, aabb.max.x-aabb.min.x) + config.ldf_set("primitiveModelValueY", LDFDataType.FLOAT, aabb.max.y-aabb.min.y) + config.ldf_set("primitiveModelValueZ", LDFDataType.FLOAT, aabb.max.z-aabb.min.z) set_vars["config"] = config self.debug_markers.append(self.server.spawn_object(14510, set_vars=set_vars)) diff --git a/luserver/modules/social.py b/luserver/modules/social.py index 25cb2ec..b5c80bc 100644 --- a/luserver/modules/social.py +++ b/luserver/modules/social.py @@ -87,7 +87,6 @@ class SocialHandling(ServerModule): response.write(c_bool(False)) # is FTP self.server.send(response, address) - def on_add_friend_response(self, response, address): assert response.read(c_int64) == 0 request_declined = response.read(c_bool) diff --git a/luserver/scripts/avant_gardens/survival/world_control.py b/luserver/scripts/avant_gardens/survival/world_control.py index 7bdc4fa..fc71144 100644 --- a/luserver/scripts/avant_gardens/survival/world_control.py +++ b/luserver/scripts/avant_gardens/survival/world_control.py @@ -1,12 +1,44 @@ +import asyncio + import luserver.components.script as script +from luserver.ldf import LDFDataType from luserver.bitstream import c_bool, c_int +from luserver.math.vector import Vector3 +from luserver.math.quaternion import Quaternion class ScriptComponent(script.ScriptComponent): + def on_startup(self): + self.script_network_vars.ldf_set("NumberOfPlayers", LDFDataType.INT32, 1) + + def set_player_spawn_points(self): + for index, player_id in enumerate(self.object.scripted_activity.players): + player = self.object._v_server.get_object(player_id) + spawn = self.object._v_server.get_objects_in_group("P%i_Spawn" % (index+1))[0] + self.object._v_server.send_game_message(player.char.teleport, ignore_y=False, pos=spawn.physics.position, set_rotation=True, x=spawn.physics.rotation.x, y=spawn.physics.rotation.y, z=spawn.physics.rotation.z, w=spawn.physics.rotation.w, address=player.char.address) + + def start(self): + self.object._v_server.send_game_message(self.object.scripted_activity.activity_start, broadcast=True) + self.script_network_vars.clear() + self.script_network_vars.ldf_set("Clear_Scoreboard", LDFDataType.BOOLEAN, True) + self.script_network_vars.ldf_set("Start_Wave_Message", LDFDataType.STRING, "Start!") + self.script_network_vars.ldf_set("wavesStarted", LDFDataType.BOOLEAN, True) + self.object._v_server.send_game_message(self.script_network_var_update, self.script_network_vars, broadcast=True) + + def player_ready(self, address): player = self.object._v_server.accounts[address].characters.selected() - script_vars = {} - script_vars["NumberOfPlayers"] = c_int, 1 - script_vars["Define_Player_To_UI"] = bytes, player.object_id - script_vars["Show_ScoreBoard"] = c_bool, int(True) - script_vars["Update_ScoreBoard_Players.1"] = bytes, player.object_id - self.object._v_server.send_game_message(self.script_network_var_update, script_vars, broadcast=True) + self.object.scripted_activity.players.append(player.object_id) + self.script_network_vars.ldf_set("Define_Player_To_UI", LDFDataType.BYTES, str(player.object_id).encode()) + self.script_network_vars.ldf_set("Show_ScoreBoard", LDFDataType.BOOLEAN, True) + self.script_network_vars.ldf_set("Update_ScoreBoard_Players.1", LDFDataType.BYTES, str(player.object_id).encode()) + self.object._v_server.send_game_message(self.script_network_var_update, self.script_network_vars, broadcast=True) + + self.set_player_spawn_points() + + def message_box_respond(self, address, button:c_int=None, identifier:"wstr"=None, user_data:"wstr"=None): + if identifier == "RePlay": + self.start() + + elif identifier == "Exit_Question" and button == 1: + player = self.object._v_server.accounts[address].characters.selected() + asyncio.ensure_future(player.char.transfer_to_last_non_instance(Vector3(131.83, 376, -180.31), Quaternion(0, -0.268720, 0, 0.963218))) diff --git a/luserver/scripts/general/transfer_to_last_non_instance.py b/luserver/scripts/general/transfer_to_last_non_instance.py index e4bf200..0d51af5 100644 --- a/luserver/scripts/general/transfer_to_last_non_instance.py +++ b/luserver/scripts/general/transfer_to_last_non_instance.py @@ -11,4 +11,4 @@ class ScriptComponent(script.ScriptComponent): def message_box_respond(self, address, button:c_int=None, identifier:"wstr"=None, user_data:"wstr"=None): if identifier == "instance_exit" and button == 1: player = self.object._v_server.accounts[address].characters.selected() - asyncio.ensure_future(player.char.transfer_to_world(((player.char.world[0] // 100)*100, player.char.world[1], player.char.world[2]))) + asyncio.ensure_future(player.char.transfer_to_last_non_instance()) diff --git a/luserver/server.py b/luserver/server.py index 48c8b81..df1b51c 100644 --- a/luserver/server.py +++ b/luserver/server.py @@ -9,7 +9,7 @@ from .modules.mail import MailID class Server(pyraknet.server.Server): NETWORK_VERSION = 171022 SERVER_PASSWORD = b"3.25 ND1" - EXPECTED_PEER_TYPE = WorldClientMsg.__int__() + EXPECTED_PEER_TYPE = WorldClientMsg.header() def __init__(self, address, max_connections, db_conn): super().__init__(address, max_connections, self.SERVER_PASSWORD) @@ -23,12 +23,12 @@ class Server(pyraknet.server.Server): def packetname(self, data): if data[0] == Message.LUPacket: - if data[1] == WorldServerMsg.__int__() and data[3] == WorldServerMsg.Routing: + if data[1] == WorldServerMsg.header() and data[3] == WorldServerMsg.Routing: data = b"\x53"+data[12:] - if (data[1], data[3]) == (WorldServerMsg.__int__(), WorldServerMsg.GameMessage) or (data[1], data[3]) == (WorldClientMsg.__int__(), WorldClientMsg.GameMessage): + if (data[1], data[3]) == (WorldServerMsg.header(), WorldServerMsg.GameMessage) or (data[1], data[3]) == (WorldClientMsg.header(), WorldClientMsg.GameMessage): message_name = GameMessage(c_ushort.unpack(data[16:18])[0]).name return "GameMessage/" + message_name - if (data[1], data[3]) == (WorldServerMsg.__int__(), WorldServerMsg.Mail) or (data[1], data[3]) == (WorldClientMsg.__int__(), WorldClientMsg.Mail): + if (data[1], data[3]) == (WorldServerMsg.header(), WorldServerMsg.Mail) or (data[1], data[3]) == (WorldClientMsg.header(), WorldClientMsg.Mail): packetname = MailID(c_uint.unpack(data[8:12])[0]).name return "Mail/" + packetname return msg_enum[data[1]](data[3]).name @@ -36,30 +36,30 @@ class Server(pyraknet.server.Server): def unknown_packetname(self, data): if data[0] == Message.LUPacket: - if data[1] == WorldServerMsg.__int__() and data[3] == WorldServerMsg.Routing: + if data[1] == WorldServerMsg.header() and data[3] == WorldServerMsg.Routing: data = b"\x53"+data[12:] - if (data[1], data[3]) == (WorldServerMsg.__int__(), WorldServerMsg.GameMessage) or (data[1], data[3]) == (WorldClientMsg.__int__(), WorldClientMsg.GameMessage): + if (data[1], data[3]) == (WorldServerMsg.header(), WorldServerMsg.GameMessage) or (data[1], data[3]) == (WorldClientMsg.header(), WorldClientMsg.GameMessage): return "GameMessage/%i" % c_ushort.unpack(data[16:18])[0] return msg_enum[data[1]].__name__ + "/%.2x" % data[3] return super().unknown_packetname(data) def packet_id(self, data): if data[0] == Message.LUPacket: - if data[1] == WorldServerMsg.__int__() and data[3] == WorldServerMsg.Routing: + if data[1] == WorldServerMsg.header() and data[3] == WorldServerMsg.Routing: return data[12], data[14] return data[1], data[3] return super().packet_id(data) def handler_data(self, data): if data[0] == Message.LUPacket: - if data[1] == WorldServerMsg.__int__() and data[3] == WorldServerMsg.Routing: + if data[1] == WorldServerMsg.header() and data[3] == WorldServerMsg.Routing: return data[19:] return data[8:] return super().handler_data(data) def register_handler(self, packet_id, callback, origin=None): if isinstance(packet_id, (GeneralMsg, AuthServerMsg, SocialMsg, WorldServerMsg, WorldClientMsg)): - header = type(packet_id).__int__() + header = type(packet_id).header() subheader = packet_id packet_id = header, subheader return super().register_handler(packet_id, callback, origin) @@ -101,11 +101,13 @@ class Server(pyraknet.server.Server): def conn_sync(self): self.conn.sync() - async def address_for_world(self, world_id): + async def address_for_world(self, world_id, include_self=False): while True: self.conn_sync() for server_address, server_world in self.db.servers.items(): - if server_world == world_id and (not hasattr(self, "external_address") or server_address != self.external_address): + if server_world == world_id: + if not include_self and hasattr(self, "external_address") and server_address == self.external_address: + continue return server_address # no server found, spawn a new one # todo: os.system probably isn't the best way to do this diff --git a/luserver/world.py b/luserver/world.py index 43f56ac..ea1b1f0 100644 --- a/luserver/world.py +++ b/luserver/world.py @@ -50,10 +50,10 @@ import time import pyraknet.replicamanager from . import amf3 -from . import ldf from . import server from .bitstream import BitStream, c_bit, c_float, c_int64, c_uint, c_ushort from .game_object import GameObject +from .ldf import LDF from .messages import GameMessage, WorldClientMsg, WorldServerMsg from .math.quaternion import Quaternion from .components.property import PropertyData, PropertySelectQueryProperty @@ -72,7 +72,7 @@ BITS_LOCAL = 1 << 46 BITS_SPAWNED = 1 << 58 | BITS_LOCAL class WorldServer(server.Server, pyraknet.replicamanager.ReplicaManager): - PEER_TYPE = WorldServerMsg.__int__() + PEER_TYPE = WorldServerMsg.header() def __init__(self, address, external_host, world_id, max_connections, db_conn): server.Server.__init__(self, address, max_connections, db_conn) @@ -288,7 +288,7 @@ class WorldServer(server.Server, pyraknet.replicamanager.ReplicaManager): return self.game_objects[object_id] elif self.world_id[0] != 0 and object_id in self.world_data.objects: return self.world_data.objects[object_id] - log.warn("Object %i not found", object_id) + log.warning("Object %i not found", object_id) def get_objects_in_group(self, group): matches = [] @@ -432,13 +432,13 @@ class WorldServer(server.Server, pyraknet.replicamanager.ReplicaManager): elif type == BitStream: out.write(c_uint(len(value))) out.write(bytes(value)) - elif type == "amf": - amf3.write(value, out) - elif type == "ldf": - ldf_text = ldf.to_ldf(value, ldf_type="text") + elif type == LDF: + ldf_text = value.to_str() out.write(ldf_text, length_type=c_uint) if ldf_text: out.write(bytes(2)) # for some reason has a null terminator + elif type == "amf": + amf3.write(value, out) elif type == "str": out.write(value, char_size=1, length_type=c_uint) elif type == "wstr": @@ -473,14 +473,14 @@ class WorldServer(server.Server, pyraknet.replicamanager.ReplicaManager): if type == BitStream: length = message.read(c_uint) return BitStream(message.read(bytes, length=length)) - if type == "amf": - return amf3.read(message) - if type == "ldf": + if type == LDF: value = message.read(str, length_type=c_uint) if value: assert message.read(c_ushort) == 0 # for some reason has a null terminator - # todo: convert to dict + # todo: convert to LDF return value + if type == "amf": + return amf3.read(message) if type == "str": return message.read(str, char_size=1, length_type=c_uint) if type == "wstr": diff --git a/runtime/db/init.py b/runtime/db/init.py index 942ed62..6ac6b1b 100644 --- a/runtime/db/init.py +++ b/runtime/db/init.py @@ -282,8 +282,8 @@ if GENERATE_COMPS: if currency_index is not None: _, minvalue, maxvalue = currency_table[currency_index][0] else: - minvalue = None - maxvalue = None + minvalue = None + maxvalue = None activity_rewards[object_template] = loot, minvalue, maxvalue diff --git a/runtime/db/luz_importer.py b/runtime/db/luz_importer.py index 7568a0e..47b0618 100644 --- a/runtime/db/luz_importer.py +++ b/runtime/db/luz_importer.py @@ -4,12 +4,12 @@ from types import SimpleNamespace import BTrees -import luserver.ldf as ldf from luserver.bitstream import BitStream, c_float, c_int, c_int64, c_ubyte, c_uint, c_uint64, c_ushort from luserver.game_object import GameObject +from luserver.ldf import LDF +from luserver.world import BITS_LOCAL, World from luserver.math.quaternion import Quaternion from luserver.math.vector import Vector3 -from luserver.world import BITS_LOCAL, World import scripts LUZ_PATHS = {} @@ -133,7 +133,7 @@ def lvl_parse_chunk_type_2001(lvl, conn, world_data, triggers): object_id |= BITS_LOCAL if lot in WHITELISTED_SERVERSIDE_LOTS: - config = ldf.from_ldf(config_data) + config = LDF(config_data) spawned_vars = {} spawned_vars["scale"] = scale @@ -187,6 +187,8 @@ def lvl_parse_chunk_type_2001(lvl, conn, world_data, triggers): spawned_vars["activity_id"] = config["activityID"] if "attached_path" in config: spawned_vars["attached_path"] = config["attached_path"] + if "collectible_id" in config: + spawned_vars["collectible_id"] = config["collectible_id"] if "rebuild_activators" in config: spawned_vars["rebuild_activator_position"] = Vector3(*(float(i) for i in config["rebuild_activators"].split("\x1f"))) if "rail_path" in config: @@ -331,7 +333,7 @@ def load_world_data(conn, maps_path): config_name = luz.read(str, length_type=c_ubyte) config_type_and_value = luz.read(str, length_type=c_ubyte) try: - config[config_name] = ldf.from_ldf_type_value(config_type_and_value) + config[config_name] = LDF.from_str_type(config_type_and_value) except ValueError: pass diff --git a/runtime/manualserver.py b/runtime/manualserver.py index 422d4df..b45be15 100644 --- a/runtime/manualserver.py +++ b/runtime/manualserver.py @@ -3,6 +3,7 @@ Modification of pyraknet.__main__ manual server, with number sorting for packet Mostly intended for manually replaying captures. """ import asyncio +import logging import os import re import threading @@ -11,6 +12,8 @@ import traceback import pyraknet.server +logging.basicConfig(format="%(levelname).1s:%(message)s", level=logging.DEBUG) + def atoi(text): return int(text) if text.isdigit() else text