From a17b143c8148bbb00aadaad34245ac79f76300ce Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Mon, 6 Feb 2023 20:27:42 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 5 + idaplugins/ffnetwork.py | 216 +++++++ idaplugins/ffutil.py | 176 ++++++ vnetlog/vnetlog.sln | 25 + vnetlog/vnetlog/MainWindow.cs | 128 +++++ vnetlog/vnetlog/OpcodeMap.cs | 35 ++ vnetlog/vnetlog/OpcodeMapBuilder.cs | 63 +++ vnetlog/vnetlog/Packet.cs | 37 ++ vnetlog/vnetlog/PacketDecoder.cs | 287 ++++++++++ vnetlog/vnetlog/PacketInterceptor.cs | 77 +++ vnetlog/vnetlog/Plugin.cs | 37 ++ vnetlog/vnetlog/ServerIPC.cs | 818 +++++++++++++++++++++++++++ vnetlog/vnetlog/Service.cs | 23 + vnetlog/vnetlog/UITree.cs | 98 ++++ vnetlog/vnetlog/packages.lock.json | 13 + vnetlog/vnetlog/vnetlog.csproj | 64 +++ vnetlog/vnetlog/vnetlog.json | 8 + 17 files changed, 2110 insertions(+) create mode 100644 .gitignore create mode 100644 idaplugins/ffnetwork.py create mode 100644 idaplugins/ffutil.py create mode 100644 vnetlog/vnetlog.sln create mode 100644 vnetlog/vnetlog/MainWindow.cs create mode 100644 vnetlog/vnetlog/OpcodeMap.cs create mode 100644 vnetlog/vnetlog/OpcodeMapBuilder.cs create mode 100644 vnetlog/vnetlog/Packet.cs create mode 100644 vnetlog/vnetlog/PacketDecoder.cs create mode 100644 vnetlog/vnetlog/PacketInterceptor.cs create mode 100644 vnetlog/vnetlog/Plugin.cs create mode 100644 vnetlog/vnetlog/ServerIPC.cs create mode 100644 vnetlog/vnetlog/Service.cs create mode 100644 vnetlog/vnetlog/UITree.cs create mode 100644 vnetlog/vnetlog/packages.lock.json create mode 100644 vnetlog/vnetlog/vnetlog.csproj create mode 100644 vnetlog/vnetlog/vnetlog.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2aabaab --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vs/ +obj/ +bin/ +*.user +logs/ diff --git a/idaplugins/ffnetwork.py b/idaplugins/ffnetwork.py new file mode 100644 index 0000000..8482f73 --- /dev/null +++ b/idaplugins/ffnetwork.py @@ -0,0 +1,216 @@ +import idaapi +import ida_ida +import ida_bytes +import ida_search + +packet_names = { + 8: 'Logout', + 14: 'CFNotify', + 111: 'Playtime', + 121: 'RSVData', + 128: 'ExamineSearchInfo', + 129: 'UpdateSearchInfo', + 141: 'Countdown', + 142: 'CountdownCancel', + 163: 'MarketBoardItemListingCount', + 164: 'MarketBoardItemListing', + 166: 'MarketBoardPurchase', + 168: 'MarketBoardItemListingHistory', + 171: 'MarketBoardSearchResult', + 173: 'FreeCompanyInfo', + 176: 'FreeCompanyDialog', + 201: 'StatusEffectList', + 202: 'StatusEffectListEureka', + 203: 'StatusEffectListBozja', + 204: 'StatusEffectListDouble', + 206: 'EffectResult1', + 207: 'EffectResult4', + 208: 'EffectResult8', + 209: 'EffectResult16', + 211: 'EffectResultBasic1', + 212: 'EffectResultBasic4', + 213: 'EffectResultBasic8', + 214: 'EffectResultBasic16', + 215: 'EffectResultBasic32', + 216: 'EffectResultBasic64', + 217: 'ActorControl', + 218: 'ActorControlSelf', + 219: 'ActorControlTarget', + 220: 'UpdateHpMpTp', + 221: 'ActionEffect1', + 224: 'ActionEffect8', + 225: 'ActionEffect16', + 226: 'ActionEffect24', + 227: 'ActionEffect32', + 230: 'StatusEffectListPlayer', + 232: 'UpdateRecastTimes', + 234: 'UpdateAllianceNormal', + 235: 'UpdateAllianceSmall', + 236: 'UpdatePartyMemberPositions', + 237: 'UpdateAllianceNormalMemberPositions', + 238: 'UpdateAllianceSmallMemberPositions', + 259: 'SpawnPlayer', + 260: 'SpawnNPC', + 261: 'SpawnBoss', + 262: 'DespawnCharacter', + 263: 'ActorMove', + 266: 'ActorSetPos', + 268: 'ActorCast', + 271: 'InitZone', + 272: 'ApplyIDScramble', + 273: 'UpdateHate', + 274: 'UpdateHater', + 275: 'SpawnObject', + 277: 'UpdateClassInfo', + 278: 'UpdateClassInfoEureka', + 279: 'UpdateClassInfoBozja', + 280: 'PlayerSetup', + 281: 'PlayerStats', + 287: 'Examine', + 294: 'RetainerInformation', + 296: 'ItemMarketBoardInfo', + 298: 'ItemInfo', + 299: 'ContainerInfo', + 300: 'InventoryTransactionFinish', + 301: 'InventoryTransaction', + 302: 'CurrencyCrystalInfo', + 304: 'InventoryActionAck', + 305: 'UpdateInventorySlot', + 318: 'EventPlay', + 319: 'EventPlay4', + 320: 'EventPlay8', + 321: 'EventPlay16', + 322: 'EventPlay32', + 323: 'EventPlay64', + 324: 'EventPlay128', + 325: 'EventPlay255', + 327: 'EventStart', + 328: 'EventFinish', + 341: 'ResultDialog', + 342: 'DesynthResult', + 391: 'EnvControl', + 397: 'SystemLogMessage1', + 398: 'SystemLogMessage2', + 399: 'SystemLogMessage4', + 400: 'SystemLogMessage8', + 401: 'SystemLogMessage16', + 419: 'WeatherChange', + 514: 'AirshipTimers', + 518: 'WaymarkPreset', + 519: 'Waymark', + 531: 'AirshipStatusList', + 532: 'AirshipStatus', + 533: 'AirshipExplorationResult', + 534: 'SubmarineStatusList', + 535: 'SubmarineProgressionStatus', + 536: 'SubmarineExplorationResult', + 538: 'SubmarineTimers', + 570: 'PrepareZoning', + 571: 'ActorGauge', + 654: 'IslandWorkshopSupplyDemand', +} + +def find_next_func_by_sig(ea, pattern): + return ida_search.find_binary(ea, ida_ida.inf_get_max_ea(), pattern, 16, ida_search.SEARCH_DOWN) + +def find_single_func_by_sig(pattern): + ea_first = find_next_func_by_sig(ida_ida.inf_get_min_ea(), pattern) + if ea_first == idaapi.BADADDR: + print(f'Could not find function by pattern {pattern}') + return 0 + if find_next_func_by_sig(ea_first + 1, pattern) != idaapi.BADADDR: + print(f'Multiple functions match pattern {pattern}') + return 0 + return ea_first + +def read_signed_byte(ea): + v = ida_bytes.get_byte(ea) + return v - 0x100 if v & 0x80 else v + +def read_signed_dword(ea): + v = ida_bytes.get_dword(ea) + return v - 0x100000000 if v & 0x80000000 else v + +def read_rva(ea): + return ea + 4 + read_signed_dword(ea) + +def get_vfoff_for_body(body): + # assume each case has the following body: + # mov rax, [rcx] + # lea r9, [r10+10h] + # jmp qword ptr [rax+] + if ida_bytes.get_byte(body) != 0x48 or ida_bytes.get_byte(body + 1) != 0x8B or ida_bytes.get_byte(body + 2) != 0x01: + return -1 + if ida_bytes.get_byte(body + 3) != 0x4D or ida_bytes.get_byte(body + 4) != 0x8D or ida_bytes.get_byte(body + 5) != 0x4A or ida_bytes.get_byte(body + 6) != 0x10: + return -1 + if ida_bytes.get_byte(body + 7) != 0x48 or ida_bytes.get_byte(body + 8) != 0xFF: + return -1 + sz = ida_bytes.get_byte(body + 9) + if sz == 0x60: + return read_signed_byte(body + 10) + elif sz == 0xA0: + return read_signed_dword(body + 10) + else: + return -1 + +def vfoff_to_index(vfoff): + if vfoff < 0x10: + return -1 # first two vfs are dtor and exec + if (vfoff & 7) != 0: + return -1 # vf contains qwords + return (vfoff >> 3) - 2 + +class ffnetwork(idaapi.plugin_t): + flags = idaapi.PLUGIN_UNL + comment = 'Build opcode map' + help = '' + wanted_name = 'ffnetwork' + wanted_hotkey = '' + + def init(self): + return idaapi.PLUGIN_OK + + def run(self, arg=None): + # assume func starts with: + # mov rax, [r8+10h] + # mov r10, [rax+38h] + # movzx eax, word ptr [r10+2] + # add eax, - + # cmp eax, + # ja + # lea r11, <__ImageBase_off> + # cdqe + # mov r9d, ds::[r11+rax*4] + func = find_single_func_by_sig('49 8B 40 10 4C 8B 50 38 41 0F B7 42 02 83 C0 ?? 3D ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 4C 8D 1D ?? ?? ?? ?? 48 98 45 8B 8C 83 ?? ?? ?? ??') + if func == 0: + return + min_case = -read_signed_byte(func + 15) # this is a negative + jumptable_size = read_signed_dword(func + 17) + 1 + def_addr = read_rva(func + 23) + imagebase = read_rva(func + 30) + jumptable = imagebase + read_signed_dword(func + 40) + opcodemap = {} + for i in range(jumptable_size): + body = imagebase + read_signed_dword(jumptable + 4 * i) + if body == def_addr: + continue + case = i + min_case + voff = get_vfoff_for_body(body) + index = vfoff_to_index(voff) + if index < 0: + print(f'Unexpected body for case {case}') + continue + if index in opcodemap: + print(f'Multiple opcodes map to single operation {index}: {hex(opcodemap[index])} and {hex(case)}') + continue + opcodemap[index] = case + for k, v in sorted(opcodemap.items()): + name = packet_names[k] if k in packet_names else f'Packet{k}' + print(f'{name} = {hex(v)}') + + def term(self): + pass + +def PLUGIN_ENTRY(): + return ffnetwork() + diff --git a/idaplugins/ffutil.py b/idaplugins/ffutil.py new file mode 100644 index 0000000..8801a47 --- /dev/null +++ b/idaplugins/ffutil.py @@ -0,0 +1,176 @@ +import idaapi +import ida_kernwin +import ida_struct +import ida_funcs +import ida_name +from PyQt5 import QtCore, QtGui, QtWidgets + +def functions(): + start_ea = 0 + while True: + nextfn = ida_funcs.get_next_func(start_ea) + if not nextfn: + break + start_ea = nextfn.start_ea + yield start_ea + +def names(): + for i in range(ida_name.get_nlist_size()): + yield (ida_name.get_nlist_ea(i), ida_name.get_nlist_name(i)) + +class dialog(QtWidgets.QDialog): + _layout = None + + def __init__(self, name): + QtWidgets.QDialog.__init__(self) + self.setWindowTitle(name) + self._layout = QtWidgets.QVBoxLayout() + self.setLayout(self._layout) + + def add_widget(self, widget): + self._layout.addWidget(widget) + return widget + + def add_layout(self, layout): + self._layout.addLayout(layout) + return layout + + def add_buttons(self, ok_name = 'OK'): + buttons = self.add_layout(QtWidgets.QHBoxLayout()) + ok = QtWidgets.QPushButton(ok_name) + ok.setDefault(True) + ok.clicked.connect(self.accept) + buttons.addWidget(ok) + cancel = QtWidgets.QPushButton('Cancel') + cancel.clicked.connect(self.reject) + buttons.addWidget(cancel) + + def run(self, ok_name = 'OK'): + self.add_buttons(ok_name) + return self.exec() + +class smart_rename(idaapi.action_handler_t): + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + highlight = ida_kernwin.get_highlight(ida_kernwin.get_current_viewer()) + struct = ida_struct.get_struc_id(highlight[0]) if highlight else idaapi.BADADDR + if struct == idaapi.BADADDR: + print('Select a structure to rename') + return 0 + + original_name = ida_struct.get_struc_name(struct) + related_names = self._related_names(original_name) + + dlg = dialog('Rename class and members') + dlg_rename = dlg.add_widget(QtWidgets.QLineEdit(original_name)) + + dlg_items = dlg.add_widget(QtWidgets.QListWidget()) + dlg_items_list = [] + for ea, name in related_names: + item = QtWidgets.QListWidgetItem(name, dlg_items) + item.setData(QtCore.Qt.UserRole, ea) + item.setCheckState(QtCore.Qt.Checked) + dlg_items_list.append(item) + + if dlg.run('Rename') == 0: + return 0 + + new_name = dlg_rename.text() + if new_name == original_name: + print('Name is not changed, nothing to do...') + return 0 + + print(f'Renaming struct {original_name} to {new_name}') + ida_struct.set_struc_name(struct, new_name) + for item in dlg_items_list: + if item.checkState() == QtCore.Qt.Checked: + ea = item.data(QtCore.Qt.UserRole) + rel_ori_name = item.text() + rel_new_name = rel_ori_name.replace(original_name, new_name) + print(f'Renaming related {rel_ori_name} to {rel_new_name}') + ida_name.set_name(ea, rel_new_name) + return 1 + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + def _related_names(self, struct_name): + return [(ea, name) for ea, name in names() if self._is_related_name(name, struct_name)] + + def _is_related_name(self, name, struct_name): + adj_name = name[5:] if name.startswith('vtbl_') else name + pos = adj_name.find(struct_name) + if pos < 0: + return False + if pos > 0 and adj_name[pos-1] != ':' and adj_name[pos-1] != '.': + return False + pos += len(struct_name) + if pos > len(adj_name): + return False + return pos == len(adj_name) or adj_name[pos] == ':' or adj_name[pos] == '.' + +class vtable_sync(idaapi.action_handler_t): + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + ea = ida_kernwin.get_screen_ea() + name = ida_name.get_name(ea) + if not name.startswith('vtbl_'): + print('Place cursor at the beginning of a virtual table and make sure it is called vtbl_ClassName') + return 0 + + name = name[5:] + vt_struct = ida_struct.get_struc_id(name + '_vtbl') + if vt_struct != idaapi.BADADDR: + return self._sync_vtable(ea, name, vt_struct) + else: + return self._create_vtable(ea, name) + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + def _sync_vtable(self, ea, classname, vtstruct): + print('todo') + return 0 + + def _create_vtable(self, ea, classname): + next_ea = min([addr for addr, name in names() if addr > ea]) + size = int((next_ea - ea) / 8) + + dlg = dialog(f'Create vtable for {classname}') + + if dlg.run('Create') == 0: + return 0 + + print(f'creating: max {size} entries') + return 0 + +class ffutil(idaapi.plugin_t): + flags = idaapi.PLUGIN_UNL + comment = 'A bunch of utilities to simplify reversing FF14' + help = '' + wanted_name = 'ffutil' + wanted_hotkey = 'Ctrl+Alt+M' + + def init(self): + print('ffutil init') + self._register_action('smart_rename', 'Rename class and all methods', smart_rename(), 'Ctrl+Shift+N') + self._register_action('vtable_sync', 'Sync vtable definitions and signatures', vtable_sync(), 'Ctrl+Shift+V') + return idaapi.PLUGIN_KEEP + + def run(self, arg=None): + print('ffutil run') + + def term(self): + print('ffutil term') + + def _register_action(self, name, label, handler, shortcut): + deco_name = 'ffutil:' + name + idaapi.unregister_action(deco_name) + idaapi.register_action(idaapi.action_desc_t(deco_name, label, handler, shortcut)) + +def PLUGIN_ENTRY(): + return ffutil() diff --git a/vnetlog/vnetlog.sln b/vnetlog/vnetlog.sln new file mode 100644 index 0000000..1250e3e --- /dev/null +++ b/vnetlog/vnetlog.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vnetlog", "vnetlog\vnetlog.csproj", "{22EF6083-B8C1-46DC-BAF8-FFD9F76C3C6B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {22EF6083-B8C1-46DC-BAF8-FFD9F76C3C6B}.Debug|x64.ActiveCfg = Debug|x64 + {22EF6083-B8C1-46DC-BAF8-FFD9F76C3C6B}.Debug|x64.Build.0 = Debug|x64 + {22EF6083-B8C1-46DC-BAF8-FFD9F76C3C6B}.Release|x64.ActiveCfg = Release|x64 + {22EF6083-B8C1-46DC-BAF8-FFD9F76C3C6B}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F4AA702C-869D-44DC-A1A0-6CB909B4ACFC} + EndGlobalSection +EndGlobal diff --git a/vnetlog/vnetlog/MainWindow.cs b/vnetlog/vnetlog/MainWindow.cs new file mode 100644 index 0000000..dd19bde --- /dev/null +++ b/vnetlog/vnetlog/MainWindow.cs @@ -0,0 +1,128 @@ +using Dalamud.Interface.Windowing; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Netlog; + +class MainWindow : Window, IDisposable +{ + private UITree _tree = new(); + private PacketDecoder _decoder = new(); + private PacketInterceptor _interceptor; + private HashSet _hiddenPackets = new(); + private bool _showRecvTime; + private bool _showPacketTarget; + private bool _showUnknown = true; + private DateTime _referenceTime; + + public MainWindow() : base("Netlog", ImGuiWindowFlags.None) + { + Namespace = "vnetlog"; + _interceptor = new(_decoder); + } + + public void Dispose() + { + _interceptor?.Dispose(); + } + + public override void Draw() + { + if (ImGui.Button("Clear")) + _interceptor.Output.Clear(); + ImGui.SameLine(); + if (ImGui.Button("Copy to clipboard")) + ImGui.SetClipboardText(DumpFilteredPackets()); + ImGui.SameLine(); + if (ImGui.Button(_interceptor.Active ? "Stop" : "Start")) + { + if (_interceptor.Active) + _interceptor.Disable(); + else + _interceptor.Enable(); + } + + foreach (var n in _tree.Node("Packet id -> opcode")) + for (int id = 0; id < _decoder.OpcodeMap.IDToOpcode.Count; ++id) + if (_decoder.OpcodeMap.IDToOpcode[id] is var opcode && opcode >= 0) + _tree.LeafNode($"{(ServerIPC.PacketID)id} == 0x{opcode:X4}"); + foreach (var n in _tree.Node("Packet opcode -> id")) + for (int opcode = 0; opcode < _decoder.OpcodeMap.OpcodeToID.Count; ++opcode) + if (_decoder.OpcodeMap.OpcodeToID[opcode] is var id && id >= 0) + _tree.LeafNode($"Opcode 0x{opcode:X4} == {(ServerIPC.PacketID)id}"); + _tree.LeafNode($"ID scramble delta: {_decoder.NetScrambleDelta} (== {_decoder.NetOffsetAdjusted} - {_decoder.NetOffsetBaseFixed} - {_decoder.NetOffsetBaseChanging})"); + + foreach (var n in _tree.Node($"Captured packets ({_interceptor.Output.Count})###packets", _interceptor.Output.Count == 0, 0xffffffff, ContextMenuCaptured)) + { + foreach (var p in _tree.Nodes(FilteredCapturedPackets(), p => new($"{PacketTime(p.ts)} #{p.i}: {p.text}###{p.i}", (p.subnodes?.Count ?? 0) == 0), p => ContextMenuPacket(p.opcode), null, p => _referenceTime = p.ts)) + { + DrawDecodedChildren(p.subnodes); + } + } + } + + private IEnumerable<(int i, DateTime ts, int opcode, string text, List? subnodes)> FilteredCapturedPackets() + { + for (int i = 0; i < _interceptor.Output.Count; ++i) + { + var p = _interceptor.Output[i]; + if (!_showUnknown && !p.Decodable) + continue; + if (_hiddenPackets.Contains(p.Opcode)) + continue; + + var ts = _showRecvTime ? p.RecvTime : p.SendTime; + var actors = _showPacketTarget ? $"{p.SourceString}->{p.TargetString}" : $"{p.SourceString}"; + var text = $"{_decoder.OpcodeMap.ID(p.Opcode)} (size={p.Payload.Length}, 0x{p.Opcode:X4} {actors}): {p.PayloadStrings.Text}"; + yield return (i, ts, p.Opcode, text, p.PayloadStrings.Children); + } + } + + private string PacketTime(DateTime ts) => ImGui.GetIO().KeyShift && _referenceTime != default ? $"{(ts - _referenceTime).TotalSeconds:f3}" : $"{ts:O}"; + + private void ContextMenuCaptured() + { + ImGui.MenuItem("Show recv timestamp instead of send timestamp", "", ref _showRecvTime); + ImGui.MenuItem("Show packet target (always player ID afaik)", "", ref _showPacketTarget); + ImGui.MenuItem("Show unknown packets", "", ref _showUnknown); + } + + private void ContextMenuPacket(int opcode) + { + if (ImGui.MenuItem("Clear filters")) + _hiddenPackets.Clear(); + if (ImGui.MenuItem($"Hide packet {opcode:X4}")) + _hiddenPackets.Add(opcode); + } + + private void DrawDecodedChildren(List? list) + { + if (list != null) + foreach (var n in _tree.Nodes(list, e => new(e.Text, (e.Children?.Count ?? 0) == 0))) + DrawDecodedChildren(n.Children); + } + + private string DumpFilteredPackets() + { + var res = new StringBuilder(); + foreach (var p in FilteredCapturedPackets()) + { + res.AppendLine($"{PacketTime(p.ts)} #{p.i}: {p.text}"); + DumpNodeChildren(res, p.subnodes, "-"); + } + return res.ToString(); + } + + private void DumpNodeChildren(StringBuilder sb, List? list, string prefix) + { + if (list == null) + return; + foreach (var n in list) + { + sb.AppendLine($"{prefix} {n.Text}"); + DumpNodeChildren(sb, n.Children, prefix + "-"); + } + } +} diff --git a/vnetlog/vnetlog/OpcodeMap.cs b/vnetlog/vnetlog/OpcodeMap.cs new file mode 100644 index 0000000..297ec88 --- /dev/null +++ b/vnetlog/vnetlog/OpcodeMap.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Netlog; + +// map from network message opcodes (which are randomized every build) to more-or-less stable indices +public class OpcodeMap +{ + private List _opcodeToID = new(); + private List _idToOpcode = new(); + + public IReadOnlyList OpcodeToID => _opcodeToID; + public IReadOnlyList IDToOpcode => _idToOpcode; + + public ServerIPC.PacketID ID(int opcode) => (ServerIPC.PacketID)(opcode >= 0 && opcode < _opcodeToID.Count ? _opcodeToID[opcode] : -1); + public int Opcode(ServerIPC.PacketID id) => (int)id >= 0 && (int)id < _idToOpcode.Count ? _idToOpcode[(int)id] : -1; + + public void AddMapping(int opcode, int id) + { + if (!AddEntry(_opcodeToID, opcode, id)) + Service.LogWarn($"[OpcodeMap] Trying to define several mappings for opcode {opcode} ({ID(opcode)} and ({(ServerIPC.PacketID)id})"); + if (!AddEntry(_idToOpcode, id, opcode)) + Service.LogWarn($"[OpcodeMap] Trying to map multiple opcodes to same index {(ServerIPC.PacketID)id} ({_idToOpcode[id]} and {opcode})"); + } + + private static bool AddEntry(List list, int index, int value) + { + if (list.Count <= index) + list.AddRange(Enumerable.Repeat(-1, index + 1 - list.Count)); + if (list[index] != -1) + return false; + list[index] = value; + return true; + } +} diff --git a/vnetlog/vnetlog/OpcodeMapBuilder.cs b/vnetlog/vnetlog/OpcodeMapBuilder.cs new file mode 100644 index 0000000..795487a --- /dev/null +++ b/vnetlog/vnetlog/OpcodeMapBuilder.cs @@ -0,0 +1,63 @@ +namespace Netlog; + +unsafe static class OpcodeMapBuilder +{ + public static OpcodeMap Build() + { + // look for an internal tracing function - it's a giant switch on opcode that calls virtual function corresponding to the opcode; we use vf indices as 'opcode index' + // function starts with: + // mov rax, [r8+10h] + // mov r10, [rax+38h] + // movzx eax, word ptr [r10+2] + // add eax, - + // cmp eax, + // ja + // lea r11, <__ImageBase_off> + // cdqe + // mov r9d, ds::[r11+rax*4] + var func = (byte*)Service.SigScanner.ScanText("49 8B 40 10 4C 8B 50 38 41 0F B7 42 02 83 C0 ?? 3D ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 4C 8D 1D ?? ?? ?? ?? 48 98 45 8B 8C 83 ?? ?? ?? ??"); + var minCase = -*(sbyte*)(func + 15); + var jumptableSize = *(int*)(func + 17) + 1; + var defaultAddr = ReadRVA(func + 23); + var imagebase = ReadRVA(func + 30); + var jumptable = (int*)(imagebase + *(int*)(func + 40)); + OpcodeMap res = new(); + for (int i = 0; i < jumptableSize; ++i) + { + var bodyAddr = imagebase + jumptable[i]; + if (bodyAddr == defaultAddr) + continue; + + var opcode = minCase + i; + var index = ReadIndexForCaseBody(bodyAddr); + if (index < 0) + Service.LogWarn($"[OpcodeMap] Unexpected body for opcode {opcode}"); + else + res.AddMapping(opcode, index); + } + return res; + } + + private static byte* ReadRVA(byte* p) => p + 4 + *(int*)p; + + // assume each case has the following body: + // mov rax, [rcx] + // lea r9, [r10+10h] + // jmp qword ptr [rax+] + private static byte[] BodyPrefix = { 0x48, 0x8B, 0x01, 0x4D, 0x8D, 0x4A, 0x10, 0x48, 0xFF }; + private static int ReadIndexForCaseBody(byte* bodyAddr) + { + for (int i = 0; i < BodyPrefix.Length; ++i) + if (bodyAddr[i] != BodyPrefix[i]) + return -1; + var vtoff = bodyAddr[BodyPrefix.Length] switch + { + 0x60 => *(bodyAddr + BodyPrefix.Length + 1), + 0xA0 => *(int*)(bodyAddr + BodyPrefix.Length + 1), + _ => -1 + }; + if (vtoff < 0x10 || (vtoff & 7) != 0) + return -1; // first two vfs are dtor and exec, vtable contains qwords + return (vtoff >> 3) - 2; + } +} diff --git a/vnetlog/vnetlog/Packet.cs b/vnetlog/vnetlog/Packet.cs new file mode 100644 index 0000000..161194f --- /dev/null +++ b/vnetlog/vnetlog/Packet.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace Netlog; + +public class TextNode +{ + public string Text; + public List? Children; + + public TextNode(string text) + { + Text = text; + } + + public TextNode AddChild(string text) + { + var child = new TextNode(text); + Children ??= new(); + Children.Add(child); + return child; + } +} + +public struct Packet +{ + public DateTime RecvTime; + public DateTime SendTime; + public uint Source; + public uint Target; + public ushort Opcode; + public bool Decodable; + public byte[] Payload; // without ipc header! + public string SourceString; + public string TargetString; + public TextNode PayloadStrings; +} diff --git a/vnetlog/vnetlog/PacketDecoder.cs b/vnetlog/vnetlog/PacketDecoder.cs new file mode 100644 index 0000000..45d560f --- /dev/null +++ b/vnetlog/vnetlog/PacketDecoder.cs @@ -0,0 +1,287 @@ +using FFXIVClientStructs.FFXIV.Client.Game; +using System; +using System.Numerics; +using Netlog.ServerIPC; +using System.Text; +using Dalamud.Memory; +using Dalamud.Utility; + +namespace Netlog; + +// utilities for decoding packets; passed to all decode functions +public unsafe class PacketDecoder +{ + private int* _netOffsetBaseFixed; + private int* _netOffsetBaseChanging; + private int* _netOffsetAdjusted; + + public OpcodeMap OpcodeMap; + + public int NetOffsetBaseFixed => *_netOffsetBaseFixed; // this is set to rand() % 256 + 14 on static init + public int NetOffsetBaseChanging => *_netOffsetBaseChanging; // this is set to rand() % 255 + 1 on every zone change + public int NetOffsetAdjusted => *_netOffsetAdjusted; // this is set to (rand() % base-sum) if id is not scrambled (so < base-sum) -or- to base-sum + delta calculated from packet data (if scrambled) on every zone change + public int NetScrambleDelta => Math.Max(0, NetOffsetAdjusted - NetOffsetBaseFixed - NetOffsetBaseChanging); // if >0, this delta is added to some ids in packets sent by server + + public PacketDecoder() + { + var scrambleAddr = Service.SigScanner.GetStaticAddressFromSig("44 89 05 ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 8B 05 ?? ?? ?? ?? 33 D2 44 03 05 ?? ?? ?? ?? 48 8B 5C 24"); + Service.LogInfo($"Scramble address = 0x{scrambleAddr:X}"); + _netOffsetBaseChanging = (int*)scrambleAddr; + _netOffsetAdjusted = _netOffsetBaseChanging + 1; + _netOffsetBaseFixed = _netOffsetBaseChanging + 3; + + OpcodeMap = OpcodeMapBuilder.Build(); + } + + public static string Vec3Str(Vector3 v) => $"[{v.X:f3}, {v.Y:f3}, {v.Z:f3}]"; + + public static string ObjStr(ulong objID) + { + var obj = Service.ObjectTable.SearchById(objID); + return obj != null ? $"{obj.DataId:X} '{obj.Name}' <{obj.ObjectId:X}>" : $"(not found) <{objID:X}>"; + } + + public static string LogMsgStr(uint id) => $"{id} '{Service.LuminaRow(id)?.Text}'"; + + public static string SpellStr(uint id) => Service.LuminaRow(id)?.Name ?? ""; + public static string ItemStr(uint id) + { + // see Dalamud.Game.Text.SeStringHandling.Payloads.GetAdjustedId + // TODO: id > 500000 is "collectible", >2000000 is "event" ?? + bool isHQ = id > 1000000; + string name = Service.LuminaRow(id % 1000000)?.Name ?? ""; + return $"{name}{(isHQ ? " (HQ)" : "")}"; + } + + public static string ActionNameStr(ActionType type, uint id) => type switch + { + ActionType.Spell => SpellStr(id), + ActionType.Item => ItemStr(id), + _ => $"" + }; + public static string ActionStr(ActionType type, uint id) => $"{type} {id} '{ActionNameStr(type, id)}'"; + + public static string StatusStr(uint statusID) => $"{statusID} '{Service.LuminaRow(statusID)?.Name ?? ""}'"; + public static string ClassJobAbbrev(byte classJob) => Service.LuminaRow(classJob)?.Abbreviation ?? ""; + + // coord: ((intCoord * 3.0518043) * 0.0099999998) - 1000.0 (0 => -1000, 65535 => +1000) + public static Vector3 IntToFloatCoords(ushort x, ushort y, ushort z) + { + float fx = x * (2000.0f / 65535) - 1000; + float fy = y * (2000.0f / 65535) - 1000; + float fz = z * (2000.0f / 65535) - 1000; + return new(fx, fy, fz); + } + + // rotation: 0 -> -180, 65535 -> +180 + public static float IntToFloatAngleDeg(ushort rot) + { + return rot * (360.0f / 65535) - 180; + } + + public static string ByteArrayStr(byte* p, int len) + { + var sb = new StringBuilder(len * 2 + 1); + for (int i = 0; i < len; ++i) + sb.Append($"{p[i]:X2}"); + return sb.ToString(); + } + + public void AddStatuses(TextNode res, ServerIPC.Status* list, int count, int offset = 0) + { + for (int i = 0; i < count; ++i) + { + var s = list + i; + if (s->ID != 0) + res.AddChild($"[{i + offset}] {StatusStr(s->ID)} {s->Extra:X4} {s->RemainingTime:f3}s left, from {ObjStr(s->SourceID)}"); + } + } + + public TextNode DecodeStatusEffectList(StatusEffectList* p, string extra = "") + { + var res = new TextNode($"L{p->Level} {ClassJobAbbrev(p->ClassID)}, hp={p->CurHP}/{p->MaxHP}, mp={p->CurMP}/{p->MaxMP}, shield={p->ShieldValue}%{extra}, u={p->u2:X2} {p->u3:X2} {p->u12:X4} {p->u17C:X8}"); + AddStatuses(res, (ServerIPC.Status*)p->Statuses, 30); + return res; + } + + public TextNode DecodeStatusEffectListDouble(StatusEffectListDouble* p) + { + var res = DecodeStatusEffectList(&p->Data); + AddStatuses(res, (ServerIPC.Status*)p->SecondSet, 30, 30); + return res; + } + + public TextNode DecodeStatusEffectListPlayer(StatusEffectListPlayer* p) + { + var res = new TextNode(""); + AddStatuses(res, (ServerIPC.Status*)p->Statuses, 30); + return res; + } + + public TextNode DecodeUpdateRecastTimes(UpdateRecastTimes* p) + { + var res = new TextNode(""); + for (int i = 0; i < 80; ++i) + res.AddChild($"group {i}: {p->Elapsed[i]:f3}/{p->Total[i]:f3}s"); + return res; + } + + public TextNode DecodeEffectResult(EffectResultEntry* entries, int count) + { + var res = new TextNode($"{count} entries, u={*(uint*)(entries + count):X8}"); + for (int i = 0; i < count; ++i) + { + var e = entries + i; + var resEntry = res.AddChild($"[{i}] seq={e->RelatedActionSequence}/{e->RelatedTargetIndex}, actor={ObjStr(e->ActorID)}, class={ClassJobAbbrev(e->ClassID)}, hp={e->CurHP}/{e->MaxHP}, mp={e->CurMP}, shield={e->ShieldValue}, u={e->u16:X4}"); + var cnt = Math.Min(4, (int)e->EffectCount); + var eff = (EffectResultEffect*)e->Effects; + for (int j = 0; j < cnt; ++j) + { + resEntry.AddChild($"#{eff->EffectIndex}: id={StatusStr(eff->StatusID)}, extra={eff->Extra:X2}, dur={eff->Duration:f3}s, src={ObjStr(eff->SourceID)}, pad={eff->pad1:X2} {eff->pad2:X4}"); + ++eff; + } + } + return res; + } + + public TextNode DecodeEffectResultBasic(EffectResultBasicEntry* entries, int count) + { + var res = new TextNode($"{count} entries, u={*(uint*)(entries + count):X8}"); + for (int i = 0; i < count; ++i) + { + var e = entries + i; + res.AddChild($"[{i}] seq={e->RelatedActionSequence}/{e->RelatedTargetIndex}, actor={ObjStr(e->ActorID)}, hp={e->CurHP}, u={e->uD:X2} {e->uE:X4}"); + } + return res; + } + + public TextNode DecodeActorControl(ActorControlCategory category, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, ulong targetID) + { + var details = category switch + { + ActorControlCategory.CancelCast => $"{ActionStr((ActionType)p2, p3)}, interrupted={p4 == 1}", // note: some successful boss casts have this message on completion, seen param1=param4=0, param2=1; param1 is related to cast time?.. + ActorControlCategory.RecastDetails => $"group {p1}: {p2 * 0.01f:f2}/{p3 * 0.01f:f2}s", + ActorControlCategory.Cooldown => $"group {p1}: action={ActionStr(ActionType.Spell, p2)}, time={p3 * 0.01f:f2}s", + ActorControlCategory.GainEffect => $"{StatusStr(p1)}: extra={p2:X4}", + ActorControlCategory.LoseEffect => $"{StatusStr(p1)}: extra={p2:X4}, source={ObjStr(p3)}, unk-update={p4 != 0}", + ActorControlCategory.UpdateEffect => $"#{p1} {StatusStr(p2)}: extra={p3:X4}", + ActorControlCategory.TargetIcon => $"{p1 - NetScrambleDelta}", + ActorControlCategory.Tether => $"#{p1}: {p2} -> {ObjStr(p3)} progress={p4}%", + ActorControlCategory.TetherCancel => $"#{p1}: {p2}", + ActorControlCategory.SetTarget => $"{ObjStr(targetID)}", + ActorControlCategory.SetAnimationState => $"#{p1} = {p2}", + ActorControlCategory.SetModelState => $"{p1}", + ActorControlCategory.PlayActionTimeline => $"{p1:X4}", + ActorControlCategory.EObjSetState => $"{p1:X4}, housing={(p3 != 0 ? p4 : null)}", + ActorControlCategory.EObjAnimation => $"{p1:X4} {p2:X4}", + ActorControlCategory.ActionRejected => $"{LogMsgStr(p1)}; action={ActionStr((ActionType)p2, p3)}, recast={p4 * 0.01f:f2}/{p5 * 0.01f:f2}, src-seq={p6}", + ActorControlCategory.IncrementRecast => $"group {p1}: dt=dt={p2 * 0.01f:f2}s", + _ => "" + }; + return new TextNode($"{category} {details} ({p1:X8} {p2:X8} {p3:X8} {p4:X8} {p5:X8} {p6:X8} {ObjStr(targetID)})"); + } + + public TextNode DecodeActionEffect(ActionEffectHeader* data, ActionEffect* effects, ulong* targetIDs, uint maxTargets, Vector3 targetPos) + { + var rot = IntToFloatAngleDeg(data->rotation); + var aid = (uint)(data->actionId - NetScrambleDelta); + var res = new TextNode($"#{data->globalEffectCounter} ({data->SourceSequence}) {ActionStr(data->actionType, aid)} ({data->actionId}/{data->actionAnimationId}), animTarget={ObjStr(data->animationTargetId)}, animLock={data->animationLockTime:f3}, rot={rot:f2}, pos={Vec3Str(targetPos)}, var={data->variation}, someTarget={ObjStr(data->SomeTargetID)}, u={data->unknown20:X2} {data->padding21:X4}"); + res.Children = new(); + var targets = Math.Min(data->NumTargets, maxTargets); + for (int i = 0; i < targets; ++i) + { + ulong targetId = targetIDs[i]; + if (targetId == 0) + continue; + var resTarget = res.AddChild($"target {i} == {ObjStr(targetId)}"); + for (int j = 0; j < 8; ++j) + { + ActionEffect* eff = effects + (i * 8) + j; + if (eff->Type == ActionEffectType.Nothing) + continue; + resTarget.AddChild($"effect {j} == {eff->Type}, params={eff->Param0:X2} {eff->Param1:X2} {eff->Param2:X2} {eff->Param3:X2} {eff->Param4:X2} {eff->Value:X4}"); + } + } + return res; + } + + public TextNode DecodeActorCast(ActorCast* p) + { + uint aid = (uint)(p->ActionID - NetScrambleDelta); + return new($"{ActionStr(p->ActionType, aid)} ({ActionStr(ActionType.Spell, p->SpellID)}) @ {ObjStr(p->TargetID)}, time={p->CastTime:f3} ({p->BaseCastTime100ms * 0.1f:f1}), rot={IntToFloatAngleDeg(p->Rotation):f2}, targetpos={Vec3Str(IntToFloatCoords(p->PosX, p->PosY, p->PosZ))}, interruptible={p->Interruptible}, u1={p->u1:X2}, u2={ObjStr(p->u2_objID)}, u3={p->u3:X4}"); + } + + public TextNode DecodeUpdateHate(UpdateHate* p) + { + var res = new TextNode($"{p->NumEntries} entries, pad={p->pad1:X2} {p->pad2:X4} {p->pad3:X8}"); + var e = (UpdateHateEntry*)p->Entries; + for (int i = 0, cnt = Math.Min((int)p->NumEntries, 8); i < cnt; ++i, ++e) + res.AddChild($"{ObjStr(e->ObjectID)} = {e->Enmity}%"); + return res; + } + + public TextNode DecodeUpdateHater(UpdateHater* p) + { + var res = new TextNode($"{p->NumEntries} entries, pad={p->pad1:X2} {p->pad2:X4} {p->pad3:X8}"); + var e = (UpdateHateEntry*)p->Entries; + for (int i = 0, cnt = Math.Min((int)p->NumEntries, 32); i < cnt; ++i, ++e) + res.AddChild($"{ObjStr(e->ObjectID)} = {e->Enmity}%"); + return res; + } + + public TextNode DecodeUpdateClassInfo(UpdateClassInfo* p, string extra = "") => new($"L{p->CurLevel}/{p->ClassLevel}/{p->SyncedLevel} {ClassJobAbbrev(p->ClassID)}, exp={p->CurExp}+{p->RestedExp}{extra}"); + + public TextNode DecodeWaymarkPreset(WaymarkPreset* p) + { + var res = new TextNode($"pad={p->pad1:X2} {p->pad2:X4}"); + for (int i = 0; i < 8; ++i) + res.AddChild($"{(WaymarkID)i}: {(p->Mask & (1 << i)) != 0} at {Vec3Str(new(p->PosX[i] * 0.001f, p->PosY[i] * 0.001f, p->PosZ[i] * 0.001f))}"); + return res; + } + public TextNode DecodeWaymark(Waymark* p) => new TextNode($"{p->ID}: {p->Active != 0} at {Vec3Str(new(p->PosX * 0.001f, p->PosY * 0.001f, p->PosZ * 0.001f))}, pad={p->pad2:X4}"); + + public TextNode? DecodePacket(ushort opcode, byte* payload, int length) => OpcodeMap.ID(opcode) switch + { + PacketID.RSVData when (RSVData*)payload is var p => new($"{MemoryHelper.ReadStringNullTerminated((nint)p->Key)} = {MemoryHelper.ReadString((nint)p->Value, p->ValueLength)} [{p->ValueLength}]"), + PacketID.Countdown when (Countdown*)payload is var p => new($"{p->Time}s from {ObjStr(p->SenderID)}{(p->FailedInCombat != 0 ? " fail-in-combat" : "")} '{MemoryHelper.ReadStringNullTerminated((nint)p->Text)}' u={p->u4:X4} {p->u9:X2} {p->u10:X2}"), + PacketID.CountdownCancel when (CountdownCancel*)payload is var p => new($"from {ObjStr(p->SenderID)} '{MemoryHelper.ReadStringNullTerminated((nint)p->Text)}' u={p->u4:X4} {p->u6:X4}"), + PacketID.StatusEffectList when (StatusEffectList*)payload is var p => DecodeStatusEffectList(p), + PacketID.StatusEffectListEureka when (StatusEffectListEureka*)payload is var p => DecodeStatusEffectList(&p->Data, $", rank={p->Rank}/{p->Element}/{p->u2}, pad={p->pad3:X2}"), + PacketID.StatusEffectListBozja when (StatusEffectListBozja*)payload is var p => DecodeStatusEffectList(&p->Data, $", rank={p->Rank}, pad={p->pad1:X2}{p->pad2:X4}"), + PacketID.StatusEffectListDouble when (StatusEffectListDouble*)payload is var p => DecodeStatusEffectListDouble(p), + PacketID.EffectResult1 when (EffectResultN*)payload is var p => DecodeEffectResult((EffectResultEntry*)p->Entries, Math.Min((int)p->NumEntries, 1)), + PacketID.EffectResult4 when (EffectResultN*)payload is var p => DecodeEffectResult((EffectResultEntry*)p->Entries, Math.Min((int)p->NumEntries, 4)), + PacketID.EffectResult8 when (EffectResultN*)payload is var p => DecodeEffectResult((EffectResultEntry*)p->Entries, Math.Min((int)p->NumEntries, 8)), + PacketID.EffectResult16 when (EffectResultN*)payload is var p => DecodeEffectResult((EffectResultEntry*)p->Entries, Math.Min((int)p->NumEntries, 16)), + PacketID.EffectResultBasic1 when (EffectResultBasicN*)payload is var p => DecodeEffectResultBasic((EffectResultBasicEntry*)p->Entries, Math.Min((int)p->NumEntries, 1)), + PacketID.EffectResultBasic4 when (EffectResultBasicN*)payload is var p => DecodeEffectResultBasic((EffectResultBasicEntry*)p->Entries, Math.Min((int)p->NumEntries, 4)), + PacketID.EffectResultBasic8 when (EffectResultBasicN*)payload is var p => DecodeEffectResultBasic((EffectResultBasicEntry*)p->Entries, Math.Min((int)p->NumEntries, 8)), + PacketID.EffectResultBasic16 when (EffectResultBasicN*)payload is var p => DecodeEffectResultBasic((EffectResultBasicEntry*)p->Entries, Math.Min((int)p->NumEntries, 16)), + PacketID.EffectResultBasic32 when (EffectResultBasicN*)payload is var p => DecodeEffectResultBasic((EffectResultBasicEntry*)p->Entries, Math.Min((int)p->NumEntries, 32)), + PacketID.EffectResultBasic64 when (EffectResultBasicN*)payload is var p => DecodeEffectResultBasic((EffectResultBasicEntry*)p->Entries, Math.Min((int)p->NumEntries, 64)), + PacketID.ActorControl when (ActorControl*)payload is var p => DecodeActorControl(p->category, p->param1, p->param2, p->param3, p->param4, 0, 0, 0xE0000000), + PacketID.ActorControlSelf when (ActorControlSelf*)payload is var p => DecodeActorControl(p->category, p->param1, p->param2, p->param3, p->param4, p->param5, p->param6, 0xE0000000), + PacketID.ActorControlTarget when (ActorControlTarget*)payload is var p => DecodeActorControl(p->category, p->param1, p->param2, p->param3, p->param4, 0, 0, p->TargetID), + PacketID.UpdateHpMpTp when (UpdateHpMpTp*)payload is var p => new($"hp={p->HP}, mp={p->MP}, gp={p->GP}"), + PacketID.ActionEffect1 when (ActionEffect1*)payload is var p => DecodeActionEffect(&p->Header, (ActionEffect*)p->Effects, p->TargetID, 1, new()), + PacketID.ActionEffect8 when (ActionEffect8*)payload is var p => DecodeActionEffect(&p->Header, (ActionEffect*)p->Effects, p->TargetID, 8, IntToFloatCoords(p->TargetX, p->TargetY, p->TargetZ)), + PacketID.ActionEffect16 when (ActionEffect16*)payload is var p => DecodeActionEffect(&p->Header, (ActionEffect*)p->Effects, p->TargetID, 16, IntToFloatCoords(p->TargetX, p->TargetY, p->TargetZ)), + PacketID.ActionEffect24 when (ActionEffect24*)payload is var p => DecodeActionEffect(&p->Header, (ActionEffect*)p->Effects, p->TargetID, 24, IntToFloatCoords(p->TargetX, p->TargetY, p->TargetZ)), + PacketID.ActionEffect32 when (ActionEffect32*)payload is var p => DecodeActionEffect(&p->Header, (ActionEffect*)p->Effects, p->TargetID, 32, IntToFloatCoords(p->TargetX, p->TargetY, p->TargetZ)), + PacketID.StatusEffectListPlayer when (StatusEffectListPlayer*)payload is var p => DecodeStatusEffectListPlayer(p), + PacketID.UpdateRecastTimes when (UpdateRecastTimes*)payload is var p => DecodeUpdateRecastTimes(p), + PacketID.ActorMove when (ActorMove*)payload is var p => new($"{Vec3Str(IntToFloatCoords(p->X, p->Y, p->Z))} {IntToFloatAngleDeg(p->Rotation):f2}, anim={p->AnimationFlags:X4}/{p->AnimationSpeed}, u={p->UnknownRotation:X2} {p->Unknown:X8}"), + PacketID.ActorSetPos when (ActorSetPos*)payload is var p => new($"{Vec3Str(new(p->X, p->Y, p->Z))} {IntToFloatAngleDeg(p->Rotation):f2}, u={p->u2:X2} {p->u3:X2} {p->u4:X8} {p->u14:X8}"), + PacketID.ActorCast when (ActorCast*)payload is var p => DecodeActorCast(p), + PacketID.UpdateHate when (UpdateHate*)payload is var p => DecodeUpdateHate(p), + PacketID.UpdateHater when (UpdateHater*)payload is var p => DecodeUpdateHater(p), + PacketID.UpdateClassInfo when (UpdateClassInfo*)payload is var p => DecodeUpdateClassInfo(p), + PacketID.UpdateClassInfoEureka when (UpdateClassInfoEureka*)payload is var p => DecodeUpdateClassInfo(&p->Data, $", rank={p->Rank}/{p->Element}/{p->u2}, pad={p->pad3:X2}"), + PacketID.UpdateClassInfoBozja when (UpdateClassInfoBozja*)payload is var p => DecodeUpdateClassInfo(&p->Data, $", rank={p->Rank}, pad={p->pad1:X2}{p->pad2:X4}"), + PacketID.EnvControl when (EnvControl*)payload is var p => new($"{p->DirectorID:X8}.{p->Index} = {p->State1:X4} {p->State2:X4}, pad={p->pad9:X2} {p->padA:X4} {p->padC:X8}"), + PacketID.WaymarkPreset when (WaymarkPreset*)payload is var p => DecodeWaymarkPreset(p), + PacketID.Waymark when (Waymark*)payload is var p => DecodeWaymark(p), + PacketID.ActorGauge when (ActorGauge*)payload is var p => new($"{ClassJobAbbrev(p->ClassJobID)} = {p->Payload:X16}"), + _ => null + }; +} diff --git a/vnetlog/vnetlog/PacketInterceptor.cs b/vnetlog/vnetlog/PacketInterceptor.cs new file mode 100644 index 0000000..d5c9ed0 --- /dev/null +++ b/vnetlog/vnetlog/PacketInterceptor.cs @@ -0,0 +1,77 @@ +using Dalamud.Hooking; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Netlog; + +[StructLayout(LayoutKind.Explicit, Pack = 1)] +unsafe struct ReceivedIPCPacket +{ + [FieldOffset(0x20)] public uint SourceActor; + [FieldOffset(0x24)] public uint TargetActor; + [FieldOffset(0x30)] public ulong PacketSize; + [FieldOffset(0x38)] public ServerIPC.IPCHeader* PacketData; +} + +[StructLayout(LayoutKind.Explicit, Pack = 1)] +unsafe struct ReceivedPacket +{ + [FieldOffset(0x10)] public ReceivedIPCPacket* IPC; + [FieldOffset(0x18)] public long SendTimestamp; +} + +unsafe class PacketInterceptor : IDisposable +{ + public List Output = new(); + + private PacketDecoder _decoder; + + private delegate bool FetchReceivedPacketDelegate(void* self, ReceivedPacket* outData); + private Hook _fetchHook; + + public PacketInterceptor(PacketDecoder decoder) + { + _decoder = decoder; + + var fetchAddress = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 0F 85 ?? ?? ?? ?? 44 0F B6 64 24"); + Service.LogInfo($"Fetch address: 0x{fetchAddress:X}"); + _fetchHook = Hook.FromAddress(fetchAddress, FetchReceivedPacketDetour); + } + + public bool Active => _fetchHook.IsEnabled; + public void Enable() => _fetchHook.Enable(); + public void Disable() => _fetchHook.Disable(); + + public void Dispose() + { + _fetchHook.Dispose(); + } + + private bool FetchReceivedPacketDetour(void* self, ReceivedPacket* outData) + { + var res = _fetchHook.Original(self, outData); + if (outData->IPC != null) + { + var opcode = outData->IPC->PacketData->MessageType; + var payloadStart = (byte*)(outData->IPC->PacketData + 1); + var payloadSize = (int)outData->IPC->PacketSize - sizeof(ServerIPC.IPCHeader); + var payload = new Span(payloadStart, payloadSize).ToArray(); + var decoded = _decoder.DecodePacket(opcode, payloadStart, payloadSize); + Output.Add(new() + { + RecvTime = DateTime.UtcNow, + SendTime = DateTimeOffset.FromUnixTimeMilliseconds(outData->SendTimestamp).DateTime, + Source = outData->IPC->SourceActor, + Target = outData->IPC->TargetActor, + Opcode = opcode, + Decodable = decoded != null, + Payload = payload, + SourceString = PacketDecoder.ObjStr(outData->IPC->SourceActor), + TargetString = PacketDecoder.ObjStr(outData->IPC->TargetActor), + PayloadStrings = decoded ?? new(PacketDecoder.ByteArrayStr(payloadStart, payloadSize)) + }); + } + return res; + } +} diff --git a/vnetlog/vnetlog/Plugin.cs b/vnetlog/vnetlog/Plugin.cs new file mode 100644 index 0000000..f5ba834 --- /dev/null +++ b/vnetlog/vnetlog/Plugin.cs @@ -0,0 +1,37 @@ +using Dalamud.Game.Command; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; + +namespace Netlog; + +public sealed class Plugin : IDalamudPlugin +{ + public string Name => "VNetlog"; + + public DalamudPluginInterface Dalamud { get; init; } + private CommandManager _cmdMgr; + + public WindowSystem WindowSystem = new("VNetlog"); + private MainWindow _wndMain; + + public Plugin(DalamudPluginInterface dalamud, CommandManager cmd) + { + dalamud.Create(); + + Dalamud = dalamud; + _cmdMgr = cmd; + + _wndMain = new(); + WindowSystem.AddWindow(_wndMain); + + Dalamud.UiBuilder.Draw += WindowSystem.Draw; + Dalamud.UiBuilder.OpenConfigUi += () => _wndMain.IsOpen = true; + _cmdMgr.AddHandler("/vnetlog", new((cmd, args) => _wndMain.IsOpen = true)); + } + + public void Dispose() + { + WindowSystem.RemoveAllWindows(); + _cmdMgr.RemoveHandler("/vnetlog"); + } +} diff --git a/vnetlog/vnetlog/ServerIPC.cs b/vnetlog/vnetlog/ServerIPC.cs new file mode 100644 index 0000000..4dd3b15 --- /dev/null +++ b/vnetlog/vnetlog/ServerIPC.cs @@ -0,0 +1,818 @@ +using FFXIVClientStructs.FFXIV.Client.Game; +using System; +using System.Runtime.InteropServices; + +namespace Netlog.ServerIPC; + +public enum PacketID +{ + Logout = 8, + CFNotify = 14, + Playtime = 111, + RSVData = 121, + ExamineSearchInfo = 128, + UpdateSearchInfo = 129, + Countdown = 141, + CountdownCancel = 142, + MarketBoardItemListingCount = 163, + MarketBoardItemListing = 164, + MarketBoardPurchase = 166, + MarketBoardItemListingHistory = 168, + MarketBoardSearchResult = 171, + FreeCompanyInfo = 173, + FreeCompanyDialog = 176, + StatusEffectList = 201, + StatusEffectListEureka = 202, + StatusEffectListBozja = 203, + StatusEffectListDouble = 204, + EffectResult1 = 206, + EffectResult4 = 207, + EffectResult8 = 208, + EffectResult16 = 209, + EffectResultBasic1 = 211, + EffectResultBasic4 = 212, + EffectResultBasic8 = 213, + EffectResultBasic16 = 214, + EffectResultBasic32 = 215, + EffectResultBasic64 = 216, + ActorControl = 217, + ActorControlSelf = 218, + ActorControlTarget = 219, + UpdateHpMpTp = 220, + ActionEffect1 = 221, + ActionEffect8 = 224, + ActionEffect16 = 225, + ActionEffect24 = 226, + ActionEffect32 = 227, + StatusEffectListPlayer = 230, + UpdateRecastTimes = 232, + UpdateAllianceNormal = 234, + UpdateAllianceSmall = 235, + UpdatePartyMemberPositions = 236, + UpdateAllianceNormalMemberPositions = 237, + UpdateAllianceSmallMemberPositions = 238, + SpawnPlayer = 259, + SpawnNPC = 260, + SpawnBoss = 261, + DespawnCharacter = 262, + ActorMove = 263, + ActorSetPos = 266, + ActorCast = 268, + InitZone = 271, + ApplyIDScramble = 272, + UpdateHate = 273, + UpdateHater = 274, + SpawnObject = 275, + UpdateClassInfo = 277, + UpdateClassInfoEureka = 278, + UpdateClassInfoBozja = 279, + PlayerSetup = 280, + PlayerStats = 281, + Examine = 287, + RetainerInformation = 294, + ItemMarketBoardInfo = 296, + ItemInfo = 298, + ContainerInfo = 299, + InventoryTransactionFinish = 300, + InventoryTransaction = 301, + CurrencyCrystalInfo = 302, + InventoryActionAck = 304, + UpdateInventorySlot = 305, + EventPlay = 318, + EventPlay4 = 319, + EventPlay8 = 320, + EventPlay16 = 321, + EventPlay32 = 322, + EventPlay64 = 323, + EventPlay128 = 324, + EventPlay255 = 325, + EventStart = 327, + EventFinish = 328, + ResultDialog = 341, + DesynthResult = 342, + EnvControl = 391, + SystemLogMessage1 = 397, + SystemLogMessage2 = 398, + SystemLogMessage4 = 399, + SystemLogMessage8 = 400, + SystemLogMessage16 = 401, + WeatherChange = 419, + AirshipTimers = 514, + WaymarkPreset = 518, + Waymark = 519, + AirshipStatusList = 531, + AirshipStatus = 532, + AirshipExplorationResult = 533, + SubmarineStatusList = 534, + SubmarineProgressionStatus = 535, + SubmarineExplorationResult = 536, + SubmarineTimers = 538, + PrepareZoning = 570, + ActorGauge = 571, + IslandWorkshopSupplyDemand = 654, +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct IPCHeader +{ + public ushort Magic; // 0x0014 + public ushort MessageType; + public uint Unknown1; + public uint Epoch; + public uint Unknown2; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct RSVData +{ + public int ValueLength; + public fixed byte Key[48]; + public fixed byte Value[1]; // variable-length +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct Countdown +{ + public uint SenderID; + public ushort u4; + public ushort Time; + public byte FailedInCombat; + public byte u9; + public byte u10; + public fixed byte Text[37]; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct CountdownCancel +{ + public uint SenderID; + public ushort u4; + public ushort u6; + public fixed byte Text[32]; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Status +{ + public ushort ID; + public ushort Extra; + public float RemainingTime; + public uint SourceID; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct StatusEffectList +{ + public byte ClassID; + public byte Level; + public byte u2; + public byte u3; // != 0 => set alliance member flag 8 + public int CurHP; + public int MaxHP; + public ushort CurMP; + public ushort MaxMP; + public ushort ShieldValue; + public ushort u12; + public fixed byte Statuses[30 * 12]; // Status[30] + public uint u17C; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct StatusEffectListEureka +{ + public byte Rank; + public byte Element; + public byte u2; + public byte pad3; + public StatusEffectList Data; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct StatusEffectListBozja +{ + public byte Rank; + public byte pad1; + public ushort pad2; + public StatusEffectList Data; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct StatusEffectListDouble +{ + public fixed byte SecondSet[30 * 12]; // Status[30] + public StatusEffectList Data; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct EffectResultEffect +{ + public byte EffectIndex; + public byte pad1; + public ushort StatusID; + public ushort Extra; + public ushort pad2; + public float Duration; + public uint SourceID; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct EffectResultEntry +{ + public uint RelatedActionSequence; + public uint ActorID; + public uint CurHP; + public uint MaxHP; + public ushort CurMP; + public byte RelatedTargetIndex; + public byte ClassID; + public byte ShieldValue; + public byte EffectCount; + public ushort u16; + public fixed byte Effects[4 * 16]; // EffectResultEffect[4] +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct EffectResultN +{ + public byte NumEntries; + public byte pad1; + public ushort pad2; + public fixed byte Entries[1 * 0x58]; // N=1/4/8/16 + // followed by 1 dword padding +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct EffectResultBasicEntry +{ + public uint RelatedActionSequence; + public uint ActorID; + public uint CurHP; + public byte RelatedTargetIndex; + public byte uD; + public ushort uE; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct EffectResultBasicN +{ + public byte NumEntries; + public byte pad1; + public ushort pad2; + public fixed byte Entries[1 * 16]; // N=1/4/8/16/32/64 + // followed by 1 dword padding +} + +public enum ActorControlCategory : ushort +{ + ToggleWeapon = 0, // from dissector + AutoAttack = 1, // from dissector + SetStatus = 2, // from dissector + CastStart = 3, // from dissector + ToggleAggro = 4, // from dissector + ClassJobChange = 5, // from dissector + Death = 6, // dissector calls it DefeatMsg + GainExpMsg = 7, // from dissector + LevelUpEffect = 10, // from dissector + ExpChainMsg = 12, // from dissector + HpSetStat = 13, // from dissector + DeathAnimation = 14, // from dissector + CancelCast = 15, // dissector calls it CastInterrupt (ActorControl), machina calls it CancelAbility + RecastDetails = 16, // p1=group id, p2=elapsed, p3=total + Cooldown = 17, // dissector calls it ActionStart (ActorControlSelf) + GainEffect = 20, // note: this packet only causes log message and hit vfx to appear, it does not actually update statuses + LoseEffect = 21, + UpdateEffect = 22, + HoT_DoT = 23, // dissector calls it HPFloatingText + UpdateRestedExp = 24, // from dissector + Flee = 27, // from dissector + UnkVisControl = 30, // visibility control ??? (ActorControl, params=delay-after-spawn, visible, id, 0) + TargetIcon = 34, // dissector calls it CombatIndicationShow, this is for boss-related markers, param1 = marker id, param2=param3=param4=0 + Tether = 35, + SpawnEffect = 37, // from dissector + ToggleInvisible = 38, // from dissector + ToggleActionUnlock = 41, // from dissector + UpdateUiExp = 43, // from dissector + DmgTakenMsg = 45, // from dissector + TetherCancel = 47, + SetTarget = 50, // from dissector + Targetable = 54, // dissector calls it ToggleNameHidden + SetAnimationState = 62, // example - ASSN beacon activation; param1 = animation set index (0 or 1), param2 = animation index (0-7) + SetModelState = 63, // example - TEA liquid hand (open/closed); param1=ModelState row index, rest unused + LimitBreakStart = 71, // from dissector + LimitBreakPartyStart = 72, // from dissector + BubbleText = 73, // from dissector + DamageEffect = 80, // from dissector + RaiseAnimation = 81, // from dissector + TreasureScreenMsg = 87, // from dissector + SetOwnerId = 89, // from dissector + ItemRepairMsg = 92, // from dissector + BluActionLearn = 99, // from dissector + DirectorInit = 100, // from dissector + DirectorClear = 101, // from dissector + LeveStartAnim = 102, // from dissector + LeveStartError = 103, // from dissector + DirectorEObjMod = 106, // from dissector + DirectorUpdate = 109, + ItemObtainMsg = 117, // from dissector + DutyQuestScreenMsg = 123, // from dissector + FatePosition = 125, // from dissector + ItemObtainIcon = 132, // from dissector + FateItemFailMsg = 133, // from dissector + FateFailMsg = 134, // from dissector + ActionLearnMsg1 = 135, // from dissector + FreeEventPos = 138, // from dissector + FateSync = 139, // from dissector + DailyQuestSeed = 144, // from dissector + SetBGM = 161, // from dissector + UnlockAetherCurrentMsg = 164, // from dissector + RemoveName = 168, // from dissector + ScreenFadeOut = 170, // from dissector + ZoneIn = 200, // from dissector + ZoneInDefaultPos = 201, // from dissector + TeleportStart = 203, // from dissector + TeleportDone = 205, // from dissector + TeleportDoneFadeOut = 206, // from dissector + DespawnZoneScreenMsg = 207, // from dissector + InstanceSelectDlg = 210, // from dissector + ActorDespawnEffect = 212, // from dissector + CompanionUnlock = 253, // from dissector + ObtainBarding = 254, // from dissector + EquipBarding = 255, // from dissector + CompanionMsg1 = 258, // from dissector + CompanionMsg2 = 259, // from dissector + ShowPetHotbar = 260, // from dissector + ActionLearnMsg = 265, // from dissector + ActorFadeOut = 266, // from dissector + ActorFadeIn = 267, // from dissector + WithdrawMsg = 268, // from dissector + OrderCompanion = 269, // from dissector + ToggleCompanion = 270, // from dissector + LearnCompanion = 271, // from dissector + ActorFateOut1 = 272, // from dissector + Emote = 290, // from dissector + EmoteInterrupt = 291, // from dissector + SetPose = 295, // from dissector + FishingLightChange = 300, // from dissector + GatheringSenseMsg = 304, // from dissector + PartyMsg = 305, // from dissector + GatheringSenseMsg1 = 306, // from dissector + GatheringSenseMsg2 = 312, // from dissector + FishingMsg = 320, // from dissector + FishingTotalFishCaught = 322, // from dissector + FishingBaitMsg = 325, // from dissector + FishingReachMsg = 327, // from dissector + FishingFailMsg = 328, // from dissector + WeeklyIntervalUpdateTime = 336, // from dissector + MateriaConvertMsg = 350, // from dissector + MeldSuccessMsg = 351, // from dissector + MeldFailMsg = 352, // from dissector + MeldModeToggle = 353, // from dissector + AetherRestoreMsg = 355, // from dissector + DyeMsg = 360, // from dissector + ToggleCrestMsg = 362, // from dissector + ToggleBulkCrestMsg = 363, // from dissector + MateriaRemoveMsg = 364, // from dissector + GlamourCastMsg = 365, // from dissector + GlamourRemoveMsg = 366, // from dissector + RelicInfuseMsg = 377, // from dissector + PlayerCurrency = 378, // from dissector + AetherReductionDlg = 381, // from dissector + PlayActionTimeline = 407, // seems to be equivalent to 412?.. + EObjSetState = 409, // from dissector + Unk6 = 412, // from dissector + EObjAnimation = 413, // from dissector + SetTitle = 500, // from dissector + SetTargetSign = 502, + SetStatusIcon = 504, // from dissector + LimitBreakGauge = 505, // name from dissector + SetHomepoint = 507, // from dissector + SetFavorite = 508, // from dissector + LearnTeleport = 509, // from dissector + OpenRecommendationGuide = 512, // from dissector + ArmoryErrorMsg = 513, // from dissector + AchievementPopup = 515, // from dissector + LogMsg = 517, // from dissector + AchievementMsg = 518, // from dissector + SetItemLevel = 521, // from dissector + ChallengeEntryCompleteMsg = 523, // from dissector + ChallengeEntryUnlockMsg = 524, // from dissector + DesynthOrReductionResult = 527, // from dissector + GilTrailMsg = 529, // from dissector + HuntingLogRankUnlock = 541, // from dissector + HuntingLogEntryUpdate = 542, // from dissector + HuntingLogSectionFinish = 543, // from dissector + HuntingLogRankFinish = 544, // from dissector + SetMaxGearSets = 560, // from dissector + SetCharaGearParamUI = 608, // from dissector + ToggleWireframeRendering = 609, // from dissector + ActionRejected = 700, // from XivAlexander (ActorControlSelf) + ExamineError = 703, // from dissector + GearSetEquipMsg = 801, // from dissector + SetFestival = 902, // from dissector + ToggleOrchestrionUnlock = 918, // from dissector + SetMountSpeed = 927, // from dissector + Dismount = 929, // from dissector + BeginReplayAck = 930, // from dissector + EndReplayAck = 931, // from dissector + ShowBuildPresetUI = 1001, // from dissector + ShowEstateExternalAppearanceUI = 1002, // from dissector + ShowEstateInternalAppearanceUI = 1003, // from dissector + BuildPresetResponse = 1005, // from dissector + RemoveExteriorHousingItem = 1007, // from dissector + RemoveInteriorHousingItem = 1009, // from dissector + ShowHousingItemUI = 1015, // from dissector + HousingItemMoveConfirm = 1017, // from dissector + OpenEstateSettingsUI = 1023, // from dissector + HideAdditionalChambersDoor = 1024, // from dissector + HousingStoreroomStatus = 1049, // from dissector + TripleTriadCard = 1204, // from dissector + TripleTriadUnknown = 1205, // from dissector + FateNpc = 2351, // from dissector + FateInit = 2353, // from dissector + FateStart = 2357, // from dissector + FateEnd = 2358, // from dissector + FateProgress = 2366, // from dissector + SetPvPState = 1504, // from dissector + EndDuelSession = 1505, // from dissector + StartDuelCountdown = 1506, // from dissector + StartDuel = 1507, // from dissector + DuelResultScreen = 1508, // from dissector + SetDutyActionId = 1512, // from dissector + SetDutyActionHud = 1513, // from dissector + SetDutyActionActive = 1514, // from dissector + SetDutyActionRemaining = 1515, // from dissector + IncrementRecast = 1536, // p1=cooldown group, p2=delta time quantized to 100ms; example is brd mage ballad proc + EurekaStep = 1850, // from dissector +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActorControl +{ + public ActorControlCategory category; + public ushort padding0; + public uint param1; + public uint param2; + public uint param3; + public uint param4; + public uint padding1; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActorControlSelf +{ + public ActorControlCategory category; + public ushort padding0; + public uint param1; + public uint param2; + public uint param3; + public uint param4; + public uint param5; + public uint param6; + public uint padding1; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActorControlTarget +{ + public ActorControlCategory category; + public ushort padding0; + public uint param1; + public uint param2; + public uint param3; + public uint param4; + public uint padding1; + public ulong TargetID; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct UpdateHpMpTp +{ + public uint HP; + public ushort MP; + public ushort GP; +} + +public enum ActionEffectType : byte +{ + Nothing = 0, + Miss = 1, + FullResist = 2, + Damage = 3, + Heal = 4, + BlockedDamage = 5, + ParriedDamage = 6, + Invulnerable = 7, + NoEffectText = 8, + FailMissingStatus = 9, + MpLoss = 10, // 0x0A + MpGain = 11, // 0x0B + TpLoss = 12, // 0x0C + TpGain = 13, // 0x0D + ApplyStatusEffectTarget = 14, // 0x0E - dissector calls this "GpGain" + ApplyStatusEffectSource = 15, // 0x0F + RecoveredFromStatusEffect = 16, // 0x10 + LoseStatusEffectTarget = 17, // 0x11 + LoseStatusEffectSource = 18, // 0x12 + Unknown_13 = 19, // 0x13 - sometimes part of pvp Purify & Empyrean Rain spells, related to afflictions removal?.. + StatusNoEffect = 20, // 0x14 + ThreatPosition = 24, // 0x18 + EnmityAmountUp = 25, // 0x19 + EnmityAmountDown = 26, // 0x1A + StartActionCombo = 27, // 0x1B + Retaliation = 29, // 0x1D - 'vengeance' has value = 7, 'arms length' has value = 0 + Knockback = 32, // 0x20 + Attract1 = 33, // 0x21 + Attract2 = 34, // 0x22 + AttractCustom1 = 35, // 0x23 + AttractCustom2 = 36, // 0x24 + AttractCustom3 = 37, // 0x25 + Unknown_27 = 39, // 0x27 + Mount = 40, // 0x28 + unknown_30 = 48, // 0x30 + unknown_31 = 49, // 0x31 + Unknown_32 = 50, // 0x32 + Unknown_33 = 51, // 0x33 + FullResistStatus = 52, // 0x34 + Unknown_37 = 55, // 0x37 - 'arms length' has value = 9 on source, is this 'attack speed slow'? + Unknown_38 = 56, // 0x38 + Unknown_39 = 57, // 0x39 + VFX = 59, // 0x3B + Gauge = 60, // 0x3C + Resource = 61, // 0x3D - value 0x34 = gain war gauge (amount == hitSeverity) + Unknown_40 = 64, // 0x40 + Unknown_42 = 66, // 0x42 + Unknown_46 = 70, // 0x46 + Unknown_47 = 71, // 0x47 + SetModelState = 72, // 0x48 - value == model state + SetHP = 73, // 0x49 - e.g. zodiark's kokytos + Partial_Invulnerable = 74, // 0x4A + Interrupt = 75, // 0x4B +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActionEffect +{ + public ActionEffectType Type; + public byte Param0; + public byte Param1; + public byte Param2; + public byte Param3; + public byte Param4; + public ushort Value; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActionEffectHeader +{ + public ulong animationTargetId; // who the animation targets + public uint actionId; // what the casting player casts, shown in battle log / ui + public uint globalEffectCounter; + public float animationLockTime; + public uint SomeTargetID; + public ushort SourceSequence; // 0 = initiated by server, otherwise corresponds to client request sequence id + public ushort rotation; + public ushort actionAnimationId; + public byte variation; // animation + public ActionType actionType; + public byte unknown20; + public byte NumTargets; // machina calls it 'effectCount', but it is misleading imo + public ushort padding21; + public ushort padding22; + public ushort padding23; + public ushort padding24; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct ActionEffect1 +{ + public ActionEffectHeader Header; + public fixed ulong Effects[8]; // ActionEffect[8] + public ushort padding3; + public uint padding4; + public fixed ulong TargetID[1]; + public uint padding5; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct ActionEffect8 +{ + public ActionEffectHeader Header; + public fixed ulong Effects[8 * 8]; // ActionEffect[8 * 8] + public ushort padding3; + public uint padding4; + public fixed ulong TargetID[8]; + public ushort TargetX; + public ushort TargetY; + public ushort TargetZ; + public ushort padding5; + public uint padding6; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct ActionEffect16 +{ + public ActionEffectHeader Header; + public fixed ulong Effects[8 * 16]; // ActionEffect[8 * 16] + public ushort padding3; + public uint padding4; + public fixed ulong TargetID[16]; + public ushort TargetX; + public ushort TargetY; + public ushort TargetZ; + public ushort padding5; + public uint padding6; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct ActionEffect24 +{ + public ActionEffectHeader Header; + public fixed ulong Effects[8 * 24]; // ActionEffect[8 * 24] + public ushort padding3; + public uint padding4; + public fixed ulong TargetID[24]; + public ushort TargetX; + public ushort TargetY; + public ushort TargetZ; + public ushort padding5; + public uint padding6; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct ActionEffect32 +{ + public ActionEffectHeader Header; + public fixed ulong Effects[8 * 32]; // ActionEffect[8 * 32] + public ushort padding3; + public uint padding4; + public fixed ulong TargetID[32]; + public ushort TargetX; + public ushort TargetY; + public ushort TargetZ; + public ushort padding5; + public uint padding6; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct StatusEffectListPlayer +{ + public fixed byte Statuses[30 * 12]; // Status[30] +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct UpdateRecastTimes +{ + public fixed float Elapsed[80]; + public fixed float Total[80]; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActorMove +{ + public ushort Rotation; + public ushort AnimationFlags; + public byte AnimationSpeed; + public byte UnknownRotation; + public ushort X; + public ushort Y; + public ushort Z; + public uint Unknown; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActorSetPos +{ + public ushort Rotation; + public byte u2; + public byte u3; + public uint u4; + public float X; + public float Y; + public float Z; + public uint u14; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActorCast +{ + public ushort SpellID; + public ActionType ActionType; + public byte BaseCastTime100ms; + public uint ActionID; // also action ID; dissector calls it ItemId - matches actionId of ActionEffectHeader - e.g. when using KeyItem, action is generic 'KeyItem 1', Unknown1 is actual item id, probably similar for stuff like mounts etc. + public float CastTime; + public uint TargetID; + public ushort Rotation; + public byte Interruptible; + public byte u1; + public uint u2_objID; + public ushort PosX; + public ushort PosY; + public ushort PosZ; + public ushort u3; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct UpdateHateEntry +{ + public uint ObjectID; + public byte Enmity; + public byte pad5; + public ushort pad6; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct UpdateHate +{ + public byte NumEntries; + public byte pad1; + public ushort pad2; + public fixed ulong Entries[8]; // UpdateHateEntry[8] + public uint pad3; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct UpdateHater +{ + public byte NumEntries; + public byte pad1; + public ushort pad2; + public fixed ulong Entries[32]; // UpdateHateEntry[32] + public uint pad3; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct UpdateClassInfo +{ + public byte ClassID; + public byte pad1; + public ushort CurLevel; + public ushort ClassLevel; + public ushort SyncedLevel; + public uint CurExp; + public uint RestedExp; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct UpdateClassInfoEureka +{ + public byte Rank; + public byte Element; + public byte u2; + public byte pad3; + public UpdateClassInfo Data; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct UpdateClassInfoBozja +{ + public byte Rank; + public byte pad1; + public ushort pad2; + public UpdateClassInfo Data; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct EnvControl +{ + public uint DirectorID; + public ushort State1; // typically has 1 bit set + public ushort State2; // typically has 1 bit set + public byte Index; + public byte pad9; + public ushort padA; + public uint padC; +} + +public enum WaymarkID : byte +{ + A, B, C, D, N1, N2, N3, N4, Count +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct WaymarkPreset +{ + public byte Mask; + public byte pad1; + public ushort pad2; + public fixed int PosX[8];// Xints[0] has X of waymark A, Xints[1] X of B, etc. + public fixed int PosY[8];// To calculate 'float' coords from these you cast them to float and then divide by 1000.0 + public fixed int PosZ[8]; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public unsafe struct Waymark +{ + public WaymarkID ID; + public byte Active; // 0=off, 1=on + public ushort pad2; + public int PosX; + public int PosY;// To calculate 'float' coords from these you cast them to float and then divide by 1000.0 + public int PosZ; +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct ActorGauge +{ + public byte ClassJobID; + public ulong Payload; +} diff --git a/vnetlog/vnetlog/Service.cs b/vnetlog/vnetlog/Service.cs new file mode 100644 index 0000000..f575107 --- /dev/null +++ b/vnetlog/vnetlog/Service.cs @@ -0,0 +1,23 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects; +using Dalamud.IoC; +using Dalamud.Logging; + +namespace Netlog; + +class Service +{ + [PluginService] public static DataManager DataManager { get; private set; } = null!; + [PluginService] public static ObjectTable ObjectTable { get; private set; } = null!; + [PluginService] public static SigScanner SigScanner { get; private set; } = null!; + + public static Lumina.GameData? LuminaGameData => DataManager.GameData; + public static T? LuminaRow(uint row) where T : Lumina.Excel.ExcelRow => LuminaGameData?.GetExcelSheet(Lumina.Data.Language.English)?.GetRow(row); + + public static void LogVerbose(string msg) => PluginLog.LogVerbose(msg); + public static void LogDebug(string msg) => PluginLog.LogDebug(msg); + public static void LogInfo(string msg) => PluginLog.LogInformation(msg); + public static void LogWarn(string msg) => PluginLog.LogWarning(msg); + public static void LogError(string msg) => PluginLog.LogError(msg); +} diff --git a/vnetlog/vnetlog/UITree.cs b/vnetlog/vnetlog/UITree.cs new file mode 100644 index 0000000..482c06c --- /dev/null +++ b/vnetlog/vnetlog/UITree.cs @@ -0,0 +1,98 @@ +using ImGuiNET; +using System; +using System.Collections.Generic; + +namespace Netlog; + +public class UITree +{ + public struct NodeProperties + { + public string Text; + public bool Leaf; + public uint Color; + + public NodeProperties(string text, bool leaf = false, uint color = 0xffffffff) + { + Text = text; + Leaf = leaf; + Color = color; + } + } + + private uint _selectedID; + + // contains 0 elements (if node is closed) or single null (if node is opened) + // expected usage is 'foreach (_ in Node(...)) { draw subnodes... }' + public IEnumerable Node(string text, bool leaf = false, uint color = 0xffffffff, Action? contextMenu = null, Action? doubleClick = null, Action? select = null) + { + if (RawNode(text, leaf, color, contextMenu, doubleClick, select)) + { + yield return null; + ImGui.TreePop(); + } + ImGui.PopID(); + } + + // draw a node for each element in collection + public IEnumerable Nodes(IEnumerable collection, Func map, Action? contextMenu = null, Action? doubleClick = null, Action? select = null) + { + foreach (var t in collection) + { + var props = map(t); + if (RawNode(props.Text, props.Leaf, props.Color, contextMenu != null ? () => contextMenu(t) : null, doubleClick != null ? () => doubleClick(t) : null, select != null ? () => select(t) : null)) + { + yield return t; + ImGui.TreePop(); + } + ImGui.PopID(); + } + } + + public void LeafNode(string text, uint color = 0xffffffff, Action? contextMenu = null, Action? doubleClick = null, Action? select = null) + { + if (RawNode(text, true, color, contextMenu, doubleClick, select)) + ImGui.TreePop(); + ImGui.PopID(); + } + + // draw leaf nodes for each element in collection + public void LeafNodes(IEnumerable collection, Func map, Action? contextMenu = null, Action? doubleClick = null, Action? select = null) + { + foreach (var t in collection) + { + if (RawNode(map(t), true, 0xffffffff, contextMenu != null ? () => contextMenu(t) : null, doubleClick != null ? () => doubleClick(t) : null, select != null ? () => select(t) : null)) + ImGui.TreePop(); + ImGui.PopID(); + } + } + + // handle selection & id scopes + private bool RawNode(string text, bool leaf, uint color, Action? contextMenu, Action? doubleClick, Action? select) + { + var id = ImGui.GetID(text); + var flags = ImGuiTreeNodeFlags.None; + if (id == _selectedID) + flags |= ImGuiTreeNodeFlags.Selected; + if (leaf) + flags |= ImGuiTreeNodeFlags.Leaf; + + ImGui.PushID((int)id); + ImGui.PushStyleColor(ImGuiCol.Text, color); + bool open = ImGui.TreeNodeEx(text, flags); + ImGui.PopStyleColor(); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _selectedID = id; + select?.Invoke(); + } + if (doubleClick != null && ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left)) + doubleClick(); + if (contextMenu != null && ImGui.BeginPopupContextItem()) + { + contextMenu(); + ImGui.EndPopup(); + } + return open; + } +} diff --git a/vnetlog/vnetlog/packages.lock.json b/vnetlog/vnetlog/packages.lock.json new file mode 100644 index 0000000..2426061 --- /dev/null +++ b/vnetlog/vnetlog/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net7.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[2.1.10, )", + "resolved": "2.1.10", + "contentHash": "S6NrvvOnLgT4GDdgwuKVJjbFo+8ZEj+JsEYk9ojjOR/MMfv1dIFpT8aRJQfI24rtDcw1uF+GnSSMN4WW1yt7fw==" + } + } + } +} \ No newline at end of file diff --git a/vnetlog/vnetlog/vnetlog.csproj b/vnetlog/vnetlog/vnetlog.csproj new file mode 100644 index 0000000..e91c036 --- /dev/null +++ b/vnetlog/vnetlog/vnetlog.csproj @@ -0,0 +1,64 @@ + + + + + + 0.0.0.1 + Tools for logging, decoding and analyzing network messages. + + https://github.com/awgil/ffreverse + + + + net7.0-windows + x64 + enable + latest + true + false + false + true + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + Netlog + + + + $(DALAMUD_HOME)/ + + + + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + $(DalamudLibPath)Newtonsoft.Json.dll + false + + + $(DalamudLibPath)Dalamud.dll + false + + + $(DalamudLibPath)ImGui.NET.dll + false + + + $(DalamudLibPath)ImGuiScene.dll + false + + + $(DalamudLibPath)Lumina.dll + false + + + $(DalamudLibPath)Lumina.Excel.dll + false + + + + diff --git a/vnetlog/vnetlog/vnetlog.json b/vnetlog/vnetlog/vnetlog.json new file mode 100644 index 0000000..7adedc6 --- /dev/null +++ b/vnetlog/vnetlog/vnetlog.json @@ -0,0 +1,8 @@ +{ + "Author": "veyn", + "Name": "Network reversing toolkit", + "Punchline": "Log, decode and analyze network messages", + "Description": "A collection of utilities for reversing network messages.", + "InternalName": "vnetlog", + "ApplicableVersion": "any" +}