Skip to content

Commit

Permalink
add packet dumper
Browse files Browse the repository at this point in the history
  • Loading branch information
ny committed Mar 2, 2023
1 parent 4d4aad9 commit 0a40a1f
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 147 deletions.
25 changes: 20 additions & 5 deletions ff_draw/gui/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import glfw
import glm
import imgui
from ff_draw import plugins

if typing.TYPE_CHECKING:
from . import Drawing
Expand Down Expand Up @@ -43,7 +42,7 @@ def ffd_page(self):
else:
self.territory = f'{territory.region.text_sgl}-{territory.sub_region.text_sgl}-{territory.area.text_sgl}'
imgui.text(f'territory: {self.territory}#{tid}')
imgui.text(f'pos: {me.pos}#{me.facing/math.pi:.2f}pi')
imgui.text(f'pos: {me.pos}#{me.facing / math.pi:.2f}pi')
else:
imgui.text(f'me: N/A')

Expand Down Expand Up @@ -78,6 +77,23 @@ def ffd_page(self):
if clicked:
sniffer.config['print_packets'] = sniffer.print_packets
self.main.save_config()
clicked, sniffer.dump_pkt = imgui.checkbox("dump_pkt", sniffer.dump_pkt)
if clicked:
sniffer.config['dump_pkt'] = sniffer.dump_pkt
sniffer.update_dump()
self.main.save_config()
if sniffer.dump_pkt:
clicked, sniffer.dump_zone_down_only = imgui.checkbox("dump_zone_down_only", sniffer.dump_zone_down_only)
if clicked:
sniffer.config['dump_zone_down_only'] = sniffer.dump_zone_down_only
self.main.save_config()

imgui.text('func_parser')
parser = self.main.parser
clicked, parser.print_compile = imgui.checkbox("print_compile", parser.print_compile)
if clicked:
parser.compile_config.setdefault('print_debug', {})['enable'] = parser.print_compile
self.main.save_config()

def draw(self):
if not self.is_show:
Expand All @@ -87,9 +103,8 @@ def draw(self):
if glfw.get_window_attrib(self.window, glfw.ICONIFIED):
return
window_flag = 0
if not self.is_expand:
window_flag |= imgui.WINDOW_NO_MOVE
self.is_expand, self.is_show = imgui.begin('FFDPanel', True, flags=window_flag)
if not self.is_expand: window_flag |= imgui.WINDOW_NO_MOVE
self.is_expand, self.is_show = imgui.begin('FFDPanel', True, window_flag)
glfw.set_window_size(self.window, *map(int, imgui.get_window_size()))
if not self.is_expand: return imgui.end()
win_pos = glm.vec2(*imgui.get_window_position())
Expand Down
3 changes: 2 additions & 1 deletion ff_draw/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
else:
use_aiohttp_cors = True

from . import gui, omen, mem, func_parser, plugins, update, sniffer
from . import gui, omen, mem, func_parser, plugins, update
from .sniffer import sniffer_main as sniffer

default_cn = bool(os.environ.get('DefaultCn'))

Expand Down
140 changes: 0 additions & 140 deletions ff_draw/sniffer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,140 +0,0 @@
import logging
import multiprocessing
import os
import pathlib
import threading
import typing
from nylib.utils import serialize_data, KeyRoute, BroadcastHook
from nylib.utils.win32.network import find_process_tcp_connections
from . import enums, sniffer, extra
from .message_structs import zone_server, zone_client, chat_server, chat_client
from .utils import message, structs, simple, bundle, oodle

if typing.TYPE_CHECKING:
from ff_draw.main import FFDraw


class GameMessageBuffer:
def __init__(self, oodle_type: typing.Type[oodle.Oodle]):
self.oodle = oodle_type()
self.buffer = bytearray()

def feed(self, data: bytes):
self.buffer.extend(data)
yield from bundle.decode(self.buffer, self.oodle)


class Sniffer:
target: tuple[str, int] = None
logger = logging.getLogger('Sniffer')

def __init__(self, main: 'FFDraw'):
self.main = main
self.ipc_lock = threading.Lock()

self.config = self.main.config.setdefault('sniffer', {})
self.print_packets = self.config.setdefault('print_packets', False)
self.print_actor_control = self.config.setdefault('print_actor_control', False)
self.sniff_promisc = self.config.setdefault('sniff_promisc', True)

pno_dir = pathlib.Path(os.environ['ExcPath']) / 'res' / 'proto_no'
self._chat_server_pno_map = simple.load_pno_map(pno_dir / 'ChatServerIpc.csv', self.main.mem.game_build_date, enums.ChatServer)
self._chat_client_pno_map = simple.load_pno_map(pno_dir / 'ChatClientIpc.csv', self.main.mem.game_build_date, enums.ChatClient)
self._zone_server_pno_map = simple.load_pno_map(pno_dir / 'ZoneServerIpc.csv', self.main.mem.game_build_date, enums.ZoneServer)
self._zone_client_pno_map = simple.load_pno_map(pno_dir / 'ZoneClientIpc.csv', self.main.mem.game_build_date, enums.ZoneClient)

self.on_chat_server_message = KeyRoute(lambda m: m.proto_no)
self.on_chat_client_message = KeyRoute(lambda m: m.proto_no)
self.on_zone_server_message = KeyRoute(lambda m: m.proto_no)
self.on_zone_client_message = KeyRoute(lambda m: m.proto_no)
self.on_actor_control = KeyRoute(lambda m: m.id)
self.on_action_effect = BroadcastHook()
self.on_play_action_timeline = BroadcastHook()

self.pipe, child_pipe = multiprocessing.Pipe()
self.sniff_process = multiprocessing.Process(target=sniffer.start_sniff, args=(child_pipe, self.sniff_promisc), daemon=True)

self.packet_fix = self.main.mem.packet_fix

main.gui.timer.add_mission(self.update_tcp_target, 1, -1)
main.gui.draw_update_call.add(self.update)
self.oodles = {}
self.buffers = {}
self.extra = extra.SnifferExtra(self)

def update(self, main):
while self.pipe.poll(0):
cmd, *args = self.pipe.recv()
match cmd:
case 'ask_target':
self.pipe.send(('set_target', self.target))
case 'tcp_data':
key, is_up, data = args
if (_k := (key, is_up)) not in self.buffers:
self.buffers[_k] = buffer = GameMessageBuffer(
oodle.OodleUdp
# oodle.OodleUdp
)
else:
buffer = self.buffers[_k]
for msg in buffer.feed(data):
self._on_message(is_up, msg)

def update_tcp_target(self):
cnt = set()
t = None
for local_host, local_port, remote_host, remote_port in find_process_tcp_connections(self.main.mem.pid):
if (t := (str(remote_host), remote_port)) in cnt: break
cnt.add(t)
t = None
if t != self.target:
self.buffers.clear()
self.target = t
self.pipe.send(('set_target', t))

def _on_message(self, is_up, msg: message.BaseMessage):
if msg.el_header.type == 3:
msg = msg.to_el(structs.IpcHeader)
self._on_ipc_message(msg.element.timestamp_s != 10, is_up, msg)

def _on_ipc_message(self, is_zone, is_up, msg: message.ElementMessage[structs.IpcHeader]):
with self.ipc_lock:
if is_zone:
if is_up:
pno_map = self._zone_client_pno_map
call = self.on_zone_client_message
type_map = zone_client.type_map
else:
pno_map = self._zone_server_pno_map
call = self.on_zone_server_message
type_map = zone_server.type_map
else:
if is_up:
pno_map = self._chat_client_pno_map
call = self.on_chat_client_message
type_map = chat_client.type_map
else:
pno_map = self._chat_server_pno_map
call = self.on_chat_server_message
type_map = chat_server.type_map

data = msg.raw_data
if (pno := msg.element.proto_no) in pno_map:
pno = pno_map[pno]
if t := type_map.get(pno):
msg = msg.to_ipc(t)
data = msg.message
if hasattr(t, '_pkt_fix'):
data._pkt_fix(self.packet_fix.value)
try:
evt = message.NetworkMessage(proto_no=pno, raw_message=msg, header=msg.el_header, message=data)
if self.print_packets:
source_name = getattr(self.main.mem.actor_table.get_actor_by_id(evt.header.source_id), 'name', None)
self.logger.debug(f'{"Zone" if is_zone else "Chat"}{"Client" if is_up else "Server"}[{pno}] {source_name}#{evt.header.source_id:x} {serialize_data(data)}')
# self.logger.debug(f'{is_zone} {is_up} {evt.proto_no} {call.route.get(evt.proto_no)}')
call(evt)
except Exception as e:
self.logger.error(f'error in processing network message {pno}', exc_info=e)

def start(self):
self.sniff_process.start()
87 changes: 87 additions & 0 deletions ff_draw/sniffer/message_dump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import json
import pathlib
import struct
import threading
import time

from . import enums
from .message_structs import zone_server, zone_client, chat_server, chat_client
from .utils import simple

empty_ipc = bytearray(16)


class MessageDumper:
_ver_ = 0

def __init__(self, file_name: pathlib.Path | str, game_build_date: str):
if isinstance(file_name, str): file_name = pathlib.Path(file_name)
assert not file_name.exists()
file_name.parent.mkdir(exist_ok=True, parents=True)
self.file_name = file_name
self.game_build_date = game_build_date
self.handle = open(file_name, 'wb', buffering=0)
self.handle.write(json.dumps(self.get_header(), ensure_ascii=False).encode('utf-8') + b'\n')
self.write_lock = threading.Lock()

def get_header(self):
return {
'dumper_version': self._ver_,
'game_build_date': self.game_build_date,
'start_log_time': int(time.time() * 1000),
}

def write(self, timestamp_ms: int, is_zone: bool, is_up: bool, proto_no: int, source_id: int, data: bytes, fix_value=0):
to_write = struct.pack(b'BBHIQI',((int(is_zone) << 1) | int(is_up)) , fix_value, proto_no, source_id, timestamp_ms, len(data)) + data
with self.write_lock: self.handle.write(to_write)

def close(self):
self.handle.close()
self.handle = None

@classmethod
def parse(cls, file_name: pathlib.Path | str, pno_dir: pathlib.Path | str):
if isinstance(file_name, str): file_name = pathlib.Path(file_name)
if isinstance(pno_dir, str): pno_dir = pathlib.Path(pno_dir)

assert file_name.exists()
with open(file_name, 'rb') as buf:
header = json.loads(buf.readline().decode('utf-8'))
assert isinstance(header, dict) and header['dumper_version'] == cls._ver_
game_build_date = header['game_build_date']
chat_server_pno_map = simple.load_pno_map(pno_dir / 'ChatServerIpc.csv', game_build_date, enums.ChatServer)
chat_client_pno_map = simple.load_pno_map(pno_dir / 'ChatClientIpc.csv', game_build_date, enums.ChatClient)
zone_server_pno_map = simple.load_pno_map(pno_dir / 'ZoneServerIpc.csv', game_build_date, enums.ZoneServer)
zone_client_pno_map = simple.load_pno_map(pno_dir / 'ZoneClientIpc.csv', game_build_date, enums.ZoneClient)

while True:
if not (header_bytes := buf.read(20)): break

scope, fix_value, proto_no, source_id, timestamp_ms, size = struct.unpack(b'BBHIQI', header_bytes)
data = buf.read(size)

is_zone = scope & 0b10 > 0
is_up = scope & 0b1 > 0
if is_zone:
if is_up:
pno_map = zone_client_pno_map
type_map = zone_client.type_map
else:
pno_map = zone_server_pno_map
type_map = zone_server.type_map
else:
if is_up:
pno_map = chat_client_pno_map
type_map = chat_client.type_map
else:
pno_map = chat_server_pno_map
type_map = chat_server.type_map
if proto_no in pno_map:
proto_no = pno_map[proto_no]
if t := type_map.get(proto_no):
data = t.from_buffer_copy(data)
if hasattr(t, '_pkt_fix'):
data._pkt_fix(fix_value)
proto_no = proto_no.name

yield is_zone, is_up, proto_no, source_id, timestamp_ms, data
Loading

0 comments on commit 0a40a1f

Please sign in to comment.