1
Fork 0
mirror of https://github.com/awgil/ffxiv_reverse.git synced 2025-04-26 00:17:45 +00:00

Initial commit.

This commit is contained in:
Andrew Gilewsky 2023-02-06 20:27:42 +02:00
commit a17b143c81
17 changed files with 2110 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.vs/
obj/
bin/
*.user
logs/

216
idaplugins/ffnetwork.py Normal file
View file

@ -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+<vfoff>]
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, -<min_case>
# cmp eax, <max_case-min_case>
# ja <default_off>
# lea r11, <__ImageBase_off>
# cdqe
# mov r9d, ds::<jumptable_rva>[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()

176
idaplugins/ffutil.py Normal file
View file

@ -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()

25
vnetlog/vnetlog.sln Normal file
View file

@ -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

View file

@ -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<int> _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<TextNode>? 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<TextNode>? 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<TextNode>? list, string prefix)
{
if (list == null)
return;
foreach (var n in list)
{
sb.AppendLine($"{prefix} {n.Text}");
DumpNodeChildren(sb, n.Children, prefix + "-");
}
}
}

View file

@ -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<int> _opcodeToID = new();
private List<int> _idToOpcode = new();
public IReadOnlyList<int> OpcodeToID => _opcodeToID;
public IReadOnlyList<int> 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<int> 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;
}
}

View file

@ -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, -<min_case>
// cmp eax, <max_case-min_case>
// ja <default_off>
// lea r11, <__ImageBase_off>
// cdqe
// mov r9d, ds::<jumptable_rva>[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+<vfoff>]
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;
}
}

37
vnetlog/vnetlog/Packet.cs Normal file
View file

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace Netlog;
public class TextNode
{
public string Text;
public List<TextNode>? 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;
}

View file

@ -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<Lumina.Excel.GeneratedSheets.LogMessage>(id)?.Text}'";
public static string SpellStr(uint id) => Service.LuminaRow<Lumina.Excel.GeneratedSheets.Action>(id)?.Name ?? "<not found>";
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<Lumina.Excel.GeneratedSheets.Item>(id % 1000000)?.Name ?? "<not found>";
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<Lumina.Excel.GeneratedSheets.Status>(statusID)?.Name ?? "<not found>"}'";
public static string ClassJobAbbrev(byte classJob) => Service.LuminaRow<Lumina.Excel.GeneratedSheets.ClassJob>(classJob)?.Abbreviation ?? "<unknown>";
// 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
};
}

View file

@ -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<Packet> Output = new();
private PacketDecoder _decoder;
private delegate bool FetchReceivedPacketDelegate(void* self, ReceivedPacket* outData);
private Hook<FetchReceivedPacketDelegate> _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<FetchReceivedPacketDelegate>.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<byte>(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;
}
}

37
vnetlog/vnetlog/Plugin.cs Normal file
View file

@ -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<Service>();
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");
}
}

View file

@ -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;
}

View file

@ -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<T>(uint row) where T : Lumina.Excel.ExcelRow => LuminaGameData?.GetExcelSheet<T>(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);
}

98
vnetlog/vnetlog/UITree.cs Normal file
View file

@ -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<object?> 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<T> Nodes<T>(IEnumerable<T> collection, Func<T, NodeProperties> map, Action<T>? contextMenu = null, Action<T>? doubleClick = null, Action<T>? 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<T>(IEnumerable<T> collection, Func<T, string> map, Action<T>? contextMenu = null, Action<T>? doubleClick = null, Action<T>? 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;
}
}

View file

@ -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=="
}
}
}
}

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>0.0.0.1</Version>
<Description>Tools for logging, decoding and analyzing network messages.</Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/awgil/ffreverse</PackageProjectUrl>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Platforms>x64</Platforms>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
<RootNamespace>Netlog</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.10" />
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -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"
}