diff --git a/AutoSweep.csproj b/AutoSweep.csproj index d3d72eb..ef5aaf7 100644 --- a/AutoSweep.csproj +++ b/AutoSweep.csproj @@ -14,7 +14,7 @@ true true autoSweep - 1.4.2.0 + 1.4.3.0 @@ -68,7 +68,7 @@ - + diff --git a/Paissa/Client.cs b/Paissa/Client.cs index a436ebf..debe885 100644 --- a/Paissa/Client.cs +++ b/Paissa/Client.cs @@ -6,10 +6,8 @@ using System.Text; using System.Threading.Tasks; using AutoSweep.Structures; -using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.Gui; -using Dalamud.Logging; +using Dalamud.Plugin.Services; using DebounceThrottle; using Newtonsoft.Json; using WebSocketSharp; @@ -22,8 +20,9 @@ public class PaissaClient : IDisposable { private string sessionToken; // dalamud - private readonly ClientState clientState; - private readonly ChatGui chat; + private readonly IClientState clientState; + private readonly IChatGui chat; + private readonly IPluginLog log; // ingest debounce private readonly DebounceDispatcher ingestDebounceDispatcher = new DebounceDispatcher(1200); @@ -42,9 +41,10 @@ public class PaissaClient : IDisposable { public event EventHandler OnPlotUpdate; public event EventHandler OnPlotSold; - public PaissaClient(ClientState clientState, ChatGui chatGui) { + public PaissaClient(IClientState clientState, IChatGui chatGui, IPluginLog log) { this.clientState = clientState; chat = chatGui; + this.log = log; http = new HttpClient(); ReconnectWS(); } @@ -61,21 +61,22 @@ public void Dispose() { /// public async Task Hello() { PlayerCharacter player = clientState.LocalPlayer; - if (player == null) - return; + if (player == null) return; + var homeworld = player.HomeWorld.GameData; + if (homeworld == null) return; var charInfo = new Dictionary { { "cid", clientState.LocalContentId }, { "name", player.Name.ToString() }, - { "world", player.HomeWorld.GameData.Name.ToString() }, + { "world", homeworld.Name.ToString() }, { "worldId", player.HomeWorld.Id } }; string content = JsonConvert.SerializeObject(charInfo); - PluginLog.Debug(content); + log.Debug(content); var response = await Post("/hello", content, false); if (response.IsSuccessStatusCode) { string respText = await response.Content.ReadAsStringAsync(); sessionToken = JsonConvert.DeserializeObject(respText).session_token; - PluginLog.Log("Completed PaissaDB HELLO"); + log.Info("Completed PaissaDB HELLO"); } } @@ -123,7 +124,7 @@ public void PostLotteryInfo(uint worldId, ushort districtId, ushort wardId, usho /// The DistrictDetail public async Task GetDistrictDetailAsync(short worldId, short districtId) { HttpResponseMessage response = await http.GetAsync($"{apiBase}/worlds/{worldId}/{districtId}"); - PluginLog.Debug($"GET {apiBase}/worlds/{worldId}/{districtId} returned {response.StatusCode} ({response.ReasonPhrase})"); + log.Debug($"GET {apiBase}/worlds/{worldId}/{districtId} returned {response.StatusCode} ({response.ReasonPhrase})"); response.EnsureSuccessStatusCode(); string respText = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(respText); @@ -135,7 +136,7 @@ private void queueIngest(object data) { ingestDebounceDispatcher.Debounce(() => { string bulkIngestData = JsonConvert.SerializeObject(ingestDataQueue); PostFireAndForget("/ingest", bulkIngestData); - PluginLog.Debug($"Bulk ingesting {ingestDataQueue.Count} entries ({bulkIngestData.Length}B)"); + log.Debug($"Bulk ingesting {ingestDataQueue.Count} entries ({bulkIngestData.Length}B)"); ingestDataQueue.Clear(); }); } @@ -146,13 +147,13 @@ private async void PostFireAndForget(string route, string content, bool auth = t private async Task Post(string route, string content, bool auth = true, ushort retries = 5) { HttpResponseMessage response = null; - PluginLog.Verbose(content); + log.Verbose(content); for (var i = 0; i < retries; i++) { HttpRequestMessage request; if (auth) { if (sessionToken == null) { - PluginLog.LogWarning("Trying to send authed request but no session token!"); + log.Warning("Trying to send authed request but no session token!"); await Hello(); continue; } @@ -169,20 +170,20 @@ private async Task Post(string route, string content, bool } try { response = await http.SendAsync(request); - PluginLog.Debug($"{request.Method} {request.RequestUri} returned {response.StatusCode} ({response.ReasonPhrase})"); + log.Debug($"{request.Method} {request.RequestUri} returned {response.StatusCode} ({response.ReasonPhrase})"); if (!response.IsSuccessStatusCode) { string respText = await response.Content.ReadAsStringAsync(); - PluginLog.Warning($"{request.Method} {request.RequestUri} returned {response.StatusCode} ({response.ReasonPhrase}):\n{respText}"); + log.Warning($"{request.Method} {request.RequestUri} returned {response.StatusCode} ({response.ReasonPhrase}):\n{respText}"); } else { break; } } catch (Exception e) { - PluginLog.Warning(e, $"{request.Method} {request.RequestUri} raised an error:"); + log.Warning(e, $"{request.Method} {request.RequestUri} raised an error:"); } // if our request failed, exponential backoff for 2 * (i + 1) seconds if (i + 1 < retries) { int toDelay = 2000 * (i + 1) + new Random().Next(500, 1_500); - PluginLog.Warning($"Request {i} failed, waiting for {toDelay}ms before retry..."); + log.Warning($"Request {i} failed, waiting for {toDelay}ms before retry..."); await Task.Delay(toDelay); } } @@ -214,17 +215,17 @@ private void ReconnectWS() { // todo what is happening here? // https://github.com/zhudotexe/FFXIV_PaissaHouse/issues/14 } - PluginLog.Debug("ReconnectWS complete"); + log.Debug("ReconnectWS complete"); }); } private void OnWSOpen(object sender, EventArgs e) { - PluginLog.Information("WebSocket connected"); + log.Information("WebSocket connected"); } private void OnWSMessage(object sender, MessageEventArgs e) { if (!e.IsText) return; - PluginLog.Verbose($">>>> R: {e.Data}"); + log.Verbose($">>>> R: {e.Data}"); var message = JsonConvert.DeserializeObject(e.Data); switch (message.Type) { case "plot_open": @@ -239,20 +240,20 @@ private void OnWSMessage(object sender, MessageEventArgs e) { case "ping": break; default: - PluginLog.Warning($"Got unknown WS message: {e.Data}"); + log.Warning($"Got unknown WS message: {e.Data}"); break; } } private void OnWSClose(object sender, CloseEventArgs e) { - PluginLog.Information($"WebSocket closed ({e.Code}: {e.Reason})"); + log.Information($"WebSocket closed ({e.Code}: {e.Reason})"); // reconnect if unexpected close or server restarting if ((!e.WasClean || e.Code == 1012) && !disposed) WSReconnectSoon(); } private void OnWSError(object sender, ErrorEventArgs e) { - PluginLog.LogWarning(e.Exception, $"WebSocket error: {e.Message}"); + log.Warning(e.Exception, $"WebSocket error: {e.Message}"); if (!disposed) WSReconnectSoon(); } @@ -260,7 +261,7 @@ private void OnWSError(object sender, ErrorEventArgs e) { private void WSReconnectSoon() { if (ws.IsAlive) return; int t = new Random().Next(5_000, 15_000); - PluginLog.Warning($"WebSocket closed unexpectedly: will reconnect to socket in {t / 1000f:F3} seconds"); + log.Warning($"WebSocket closed unexpectedly: will reconnect to socket in {t / 1000f:F3} seconds"); Task.Run(async () => await Task.Delay(t)).ContinueWith(_ => { if (!disposed) ReconnectWS(); }); diff --git a/Paissa/LotteryObserver.cs b/Paissa/LotteryObserver.cs index 60b7f82..ae8d467 100644 --- a/Paissa/LotteryObserver.cs +++ b/Paissa/LotteryObserver.cs @@ -1,7 +1,6 @@ using System; using AutoSweep.Structures; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; using Lumina.Excel.GeneratedSheets; using Lumina.Text; @@ -22,16 +21,16 @@ long a8 ); [Signature("E8 ?? ?? ?? ?? 48 8B B4 24 ?? ?? ?? ?? 48 8B 6C 24 ?? E9", DetourName = nameof(OnPlacardSaleInfo))] - private Hook? PlacardSaleInfoHook { get; init; } + private Hook? placardSaleInfoHook; public LotteryObserver(Plugin plugin) { - SignatureHelper.Initialise(this); this.plugin = plugin; - PlacardSaleInfoHook?.Enable(); + plugin.InteropProvider.InitializeFromAttributes(this); + placardSaleInfoHook?.Enable(); } public void Dispose() { - PlacardSaleInfoHook?.Dispose(); + placardSaleInfoHook?.Dispose(); } public void OnPlacardSaleInfo( @@ -44,7 +43,7 @@ public void OnPlacardSaleInfo( IntPtr placardSaleInfoPtr, long a8 ) { - PlacardSaleInfoHook!.Original(agentBase, housingType, territoryTypeId, wardId, plotId, apartmentNumber, placardSaleInfoPtr, a8); + placardSaleInfoHook!.Original(agentBase, housingType, territoryTypeId, wardId, plotId, apartmentNumber, placardSaleInfoPtr, a8); // if the plot is owned, ignore it if (housingType != HousingType.UnownedHouse) return; @@ -52,10 +51,11 @@ long a8 PlacardSaleInfo saleInfo = PlacardSaleInfo.Read(placardSaleInfoPtr); - PluginLog.LogDebug( + plugin.PluginLog.Debug( $"Got PlacardSaleInfo: PurchaseType={saleInfo.PurchaseType}, TenantType={saleInfo.TenantType}, available={saleInfo.AvailabilityType}, until={saleInfo.PhaseEndsAt}, numEntries={saleInfo.EntryCount}"); - PluginLog.LogDebug($"unknown1={saleInfo.Unknown1}, unknown2={saleInfo.Unknown2}, unknown3={saleInfo.Unknown3}, unknown4={BitConverter.ToString(saleInfo.Unknown4)}"); - PluginLog.LogDebug( + plugin.PluginLog.Debug( + $"unknown1={saleInfo.Unknown1}, unknown2={saleInfo.Unknown2}, unknown3={saleInfo.Unknown3}, unknown4={BitConverter.ToString(saleInfo.Unknown4)}"); + plugin.PluginLog.Debug( $"housingType={housingType}, territoryTypeId={territoryTypeId}, wardId={wardId}, plotId={plotId}, apartmentNumber={apartmentNumber}, placardSaleInfoPtr={placardSaleInfoPtr}, a8={a8}"); // get information about the world from the clientstate @@ -64,7 +64,7 @@ long a8 SeString place = plugin.Territories.GetRow(territoryTypeId)?.PlaceName.Value?.Name; SeString worldName = world.Name; - PluginLog.LogInformation($"Plot {place} {wardId + 1}-{plotId + 1} ({worldName}) has {saleInfo.EntryCount} lottery entries."); + plugin.PluginLog.Info($"Plot {place} {wardId + 1}-{plotId + 1} ({worldName}) has {saleInfo.EntryCount} lottery entries."); plugin.PaissaClient.PostLotteryInfo(world.RowId, territoryTypeId, wardId, plotId, saleInfo); } diff --git a/Paissa/Utils.cs b/Paissa/Utils.cs index bcf9081..69c3249 100644 --- a/Paissa/Utils.cs +++ b/Paissa/Utils.cs @@ -1,5 +1,3 @@ -using Dalamud.Game.ClientState; -using Dalamud.Logging; using Lumina.Excel.GeneratedSheets; namespace AutoSweep.Paissa { @@ -46,7 +44,7 @@ public static bool ConfigEnabledForPlot(Plugin plugin, ushort worldId, ushort di World eventWorld = plugin.Worlds.GetRow(worldId); if (!(plugin.Configuration.AllNotifs || plugin.Configuration.HomeworldNotifs && worldId == plugin.ClientState.LocalPlayer?.HomeWorld.Id - || plugin.Configuration.DatacenterNotifs && eventWorld?.DataCenter.Row == plugin.ClientState.LocalPlayer?.HomeWorld.GameData.DataCenter.Row)) + || plugin.Configuration.DatacenterNotifs && eventWorld?.DataCenter.Row == plugin.ClientState.LocalPlayer?.HomeWorld.GameData?.DataCenter.Row)) return false; // get the district config DistrictNotifConfig districtNotifs; @@ -67,7 +65,6 @@ public static bool ConfigEnabledForPlot(Plugin plugin, ushort worldId, ushort di districtNotifs = plugin.Configuration.Empyrean; break; default: - PluginLog.Warning($"Unknown district in plot open event: {districtId}"); return false; } // what about house sizes in this district? @@ -83,7 +80,6 @@ public static bool ConfigEnabledForPlot(Plugin plugin, ushort worldId, ushort di notifEnabled = districtNotifs.Large; break; default: - PluginLog.Warning($"Unknown plot size in plot open event: {size}"); return false; } // and FC/individual purchase? diff --git a/Paissa/WardObserver.cs b/Paissa/WardObserver.cs index 4ba06c6..e60c04e 100644 --- a/Paissa/WardObserver.cs +++ b/Paissa/WardObserver.cs @@ -1,23 +1,44 @@ using System; using System.Runtime.InteropServices; using AutoSweep.Structures; -using Dalamud.Logging; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; using Lumina.Text; namespace AutoSweep.Paissa { - public class WardObserver { + public unsafe class WardObserver { private Plugin plugin; internal readonly SweepState SweepState; + private delegate void HandleHousingWardInfoDelegate( + void* agentBase, + IntPtr housingWardInfoPtr + ); + + [Signature("40 55 57 41 54 41 56 41 57 48 8D AC 24 ?? ?? ?? ?? B8", DetourName = nameof(OnHousingWardInfo))] + private Hook? housingWardInfoHook; + public WardObserver(Plugin plugin) { this.plugin = plugin; + plugin.InteropProvider.InitializeFromAttributes(this); SweepState = new SweepState(Utils.NumWardsPerDistrict); + housingWardInfoHook?.Enable(); } - public void OnHousingWardInfo(IntPtr dataPtr) { + public void Dispose() { + housingWardInfoHook?.Dispose(); + } + + public void OnHousingWardInfo( + void* agentBase, + IntPtr dataPtr + ) { + housingWardInfoHook!.Original(agentBase, dataPtr); + + if (!plugin.Configuration.Enabled) return; HousingWardInfo wardInfo = HousingWardInfo.Read(dataPtr); int serverTimestamp = Marshal.ReadInt32(dataPtr - 0x8); - PluginLog.LogDebug($"Got HousingWardInfo for ward: {wardInfo.LandIdent.WardNumber} territory: {wardInfo.LandIdent.TerritoryTypeId}"); + plugin.PluginLog.Debug($"Got HousingWardInfo for ward: {wardInfo.LandIdent.WardNumber} territory: {wardInfo.LandIdent.TerritoryTypeId}"); // if the current wardinfo is for a different district than the last swept one, print the header // or if the last sweep was > 10m ago @@ -34,7 +55,7 @@ public void OnHousingWardInfo(IntPtr dataPtr) { // if we've seen this ward already, ignore it if (SweepState.Contains(wardInfo)) { - PluginLog.LogDebug($"Skipped processing HousingWardInfo for ward: {wardInfo.LandIdent.WardNumber} because we have seen it already"); + plugin.PluginLog.Debug($"Skipped processing HousingWardInfo for ward: {wardInfo.LandIdent.WardNumber} because we have seen it already"); return; } @@ -47,7 +68,7 @@ public void OnHousingWardInfo(IntPtr dataPtr) { // if that's all the wards, display the district summary and thanks if (SweepState.IsComplete) OnFinishedDistrictSweep(wardInfo); - PluginLog.LogDebug($"Done processing HousingWardInfo for ward: {wardInfo.LandIdent.WardNumber}"); + plugin.PluginLog.Debug($"Done processing HousingWardInfo for ward: {wardInfo.LandIdent.WardNumber}"); } /// diff --git a/Plugin.cs b/Plugin.cs index e4f773c..022d193 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -1,19 +1,13 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Threading.Tasks; using AutoSweep.Paissa; using AutoSweep.Structures; -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Game.Network; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Logging; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Lumina.Excel; using Lumina.Excel.GeneratedSheets; @@ -23,13 +17,13 @@ public class Plugin : IDalamudPlugin { // frameworks/data internal readonly DalamudPluginInterface PluginInterface; - internal readonly ChatGui Chat; - internal readonly ClientState ClientState; - internal readonly CommandManager Commands; internal readonly Configuration Configuration; - internal readonly DataManager Data; - internal readonly Framework Framework; - internal readonly GameNetwork Network; + internal readonly IChatGui Chat; + internal readonly IClientState ClientState; + internal readonly ICommandManager Commands; + internal readonly IFramework Framework; + internal readonly IPluginLog PluginLog; + internal readonly IGameInteropProvider InteropProvider; internal readonly PaissaClient PaissaClient; internal readonly ExcelSheet HousingLandSets; @@ -45,20 +39,21 @@ public class Plugin : IDalamudPlugin { public Plugin( DalamudPluginInterface pi, - ChatGui chat, - GameNetwork network, - DataManager data, - CommandManager commands, - ClientState clientState, - Framework framework + IChatGui chat, + IDataManager data, + ICommandManager commands, + IClientState clientState, + IFramework framework, + IPluginLog log, + IGameInteropProvider interopProvider ) { PluginInterface = pi; Chat = chat; - Network = network; - Data = data; Commands = commands; ClientState = clientState; Framework = framework; + PluginLog = log; + InteropProvider = interopProvider; // setup Configuration = pi.GetPluginConfig() as Configuration ?? new Configuration(); @@ -78,7 +73,6 @@ Framework framework chatLinkPayload = pi.AddChatLinkHandler(0, OnChatLinkClick); // event hooks - network.NetworkMessage += OnNetworkEvent; pi.UiBuilder.Draw += DrawUI; pi.UiBuilder.OpenConfigUi += DrawConfigUI; framework.Update += OnUpdateEvent; @@ -87,16 +81,15 @@ Framework framework // paissa setup wardObserver = new WardObserver(this); lotteryObserver = new LotteryObserver(this); - PaissaClient = new PaissaClient(clientState, chat); + PaissaClient = new PaissaClient(clientState, chat, log); PaissaClient.OnPlotOpened += OnPlotOpened; PaissaClient.OnPlotUpdate += OnPlotUpdate; - PluginLog.LogDebug($"Initialization complete: configVersion={Configuration.Version}"); + PluginLog.Debug($"Initialization complete: configVersion={Configuration.Version}"); } public void Dispose() { ui.Dispose(); - Network.NetworkMessage -= OnNetworkEvent; Framework.Update -= OnUpdateEvent; ClientState.Login -= OnLogin; Commands.RemoveHandler(Utils.CommandName); @@ -104,6 +97,7 @@ public void Dispose() { PluginInterface.RemoveChatLinkHandler(); PaissaClient?.Dispose(); lotteryObserver.Dispose(); + wardObserver.Dispose(); } // ==== dalamud events ==== @@ -138,25 +132,17 @@ private void OnChatLinkClick(uint cmdId, SeString seString) { }); } - private void OnLogin(object _, EventArgs __) { + private void OnLogin() { clientNeedsHello = true; } - private void OnUpdateEvent(Framework f) { + private void OnUpdateEvent(IFramework f) { if (clientNeedsHello && ClientState?.LocalPlayer != null && PaissaClient != null) { clientNeedsHello = false; Task.Run(async () => await PaissaClient.Hello()); } } - private void OnNetworkEvent(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) { - if (!Configuration.Enabled) return; - if (direction != NetworkMessageDirection.ZoneDown) return; - if (!Data.IsDataReady) return; - if (opCode == Data.ServerOpCodes["HousingWardInfo"]) wardObserver.OnHousingWardInfo(dataPtr); - } - - // ==== paissa events ==== /// /// Hook to call when a new plot open event is received over the websocket. @@ -228,7 +214,7 @@ internal void OnFoundOpenHouse(uint worldId, uint territoryTypeId, int wardNumbe // ==== helpers ==== private void SendChatToConfiguredChannel(string message) { - Chat.PrintChat(new XivChatEntry { + Chat.Print(new XivChatEntry { Name = "[PaissaHouse]", Message = message, Type = Configuration.ChatType diff --git a/packages.lock.json b/packages.lock.json index 3b2a6e2..7395c0f 100644 --- a/packages.lock.json +++ b/packages.lock.json @@ -4,9 +4,9 @@ "net7.0-windows7.0": { "DalamudPackager": { "type": "Direct", - "requested": "[2.1.11, )", - "resolved": "2.1.11", - "contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw==" + "requested": "[2.1.12, )", + "resolved": "2.1.12", + "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" }, "DebounceThrottle": { "type": "Direct",