megamappingway/client/Plugin.cs

253 lines
7.1 KiB
C#

using System.Diagnostics;
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Text;
using Blake3;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using MessagePack;
namespace MegaMappingway;
public sealed class Plugin : IDalamudPlugin {
[PluginService]
internal static IPluginLog Log { get; private set; }
[PluginService]
internal DalamudPluginInterface Interface { get; init; }
[PluginService]
internal IFramework Framework { get; init; }
[PluginService]
internal IObjectTable ObjectTable { get; init; }
[PluginService]
internal IClientState ClientState { get; init; }
[PluginService]
internal IPartyList PartyList { get; init; }
internal Configuration Config { get; }
internal PluginUi Ui { get; }
private HttpClient Http { get; } = new();
private Stopwatch Stopwatch { get; } = new();
private Blake3HashAlgorithm Hasher { get; } = new();
public Plugin() {
this.Hasher.Initialize();
this.Framework!.Update += this.FrameworkUpdate;
this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration();
this.Ui = new PluginUi(this);
this.Stopwatch.Start();
}
public void Dispose() {
this.Ui.Dispose();
this.Framework.Update -= this.FrameworkUpdate;
this.Hasher.Dispose();
this.Http.Dispose();
}
internal void SaveConfig() {
this.Interface.SavePluginConfig(this.Config);
}
private void FrameworkUpdate(IFramework framework) {
var freq = Math.Max(1, Math.Min(60, this.Config.UpdateFrequency));
if (this.Stopwatch.Elapsed < TimeSpan.FromSeconds(freq) || this.ClientState.LocalPlayer is not { } localPlayer) {
return;
}
this.Stopwatch.Restart();
var partyId = (ulong) this.PartyList.PartyId;
var party = this.PartyList
.Select(member => member.ObjectId)
.ToList();
var territory = this.ClientState.TerritoryType;
var players = this.ObjectTable
.Where(obj => obj.ObjectKind == ObjectKind.Player && obj is PlayerCharacter)
.Cast<PlayerCharacter>()
.Where(
player => player.IsValid()
&& player.Level > 0
&& player.HomeWorld.Id != 65535
&& player.Name.TextValue.Trim().Length > 0
)
.Select(player => {
var customizeHex = string.Join("", player.Customize.Select(x => x.ToString("x2")));
var key = $"don't be creepy-{player.Name.TextValue.Trim()}-{player.HomeWorld.Id}-{customizeHex}";
var hash = this.Hasher.ComputeHash(Encoding.UTF8.GetBytes(key));
var pos = player.Position;
return new PlayerInfo(
hash,
player.HomeWorld.Id,
pos.X,
pos.Y,
pos.Z,
player.Rotation,
player.Customize,
player.Level,
player.ClassJob.Id,
player.CurrentHp,
player.MaxHp,
partyId != 0 && party.Contains(player.ObjectId) ? partyId : null
);
})
.ToList();
if (players.Count == 0) {
Log.Verbose("no players to upload");
return;
}
Task.Run(async () => {
// shuffle so first player isn't always local player
players.Shuffle();
var update = new Update(3, territory, localPlayer.CurrentWorld.Id, players);
var msgpack = MessagePackSerializer.Serialize(update, MessagePackSerializerOptions.Standard);
using var mem = new MemoryStream();
await using (var gz = new GZipStream(mem, CompressionLevel.Optimal)) {
await gz.WriteAsync(msgpack);
await gz.FlushAsync();
}
var gzipped = mem.ToArray();
ByteArrayContent content;
if (gzipped.Length < msgpack.Length) {
content = new ByteArrayContent(gzipped) {
Headers = {
ContentType = new MediaTypeHeaderValue("application/msgpack"),
ContentEncoding = { "gzip" },
},
};
} else {
content = new ByteArrayContent(msgpack) {
Headers = {
ContentType = new MediaTypeHeaderValue("application/msgpack"),
},
};
}
var req = new HttpRequestMessage(HttpMethod.Post, "https://map.anna.lgbt/api/upload") {
Content = content,
};
try {
var resp = await this.Http.SendAsync(req);
if (resp.IsSuccessStatusCode) {
Log.Verbose("uploaded successfully");
} else {
var output = await resp.Content.ReadAsStringAsync();
Log.Warning($"could not upload data ({resp.StatusCode}): {output}");
}
} catch (Exception ex) {
Log.Warning(ex, "could not upload data");
}
});
}
}
// Average gzipped payload size: 6150 bytes
// with http headers, call it 6500 bytes/req
[Serializable]
[MessagePackObject]
public struct Update {
[Key(0)]
public readonly byte Version;
[Key(1)]
public readonly uint Territory;
[Key(2)]
public readonly uint World;
[Key(3)]
public readonly List<PlayerInfo> Players;
public Update(byte version, uint territory, uint world, List<PlayerInfo> players) {
this.Version = version;
this.Territory = territory;
this.World = world;
this.Players = players;
}
}
[Serializable]
[MessagePackObject]
public struct PlayerInfo {
[Key(0)]
public readonly byte[] Hash;
[Key(1)]
public readonly uint World;
[Key(2)]
public readonly float X;
[Key(3)]
public readonly float Y;
[Key(4)]
public readonly float Z;
[Key(5)]
public readonly float W;
[Key(6)]
public readonly byte[] Customize;
[Key(7)]
public readonly byte Level;
[Key(8)]
public readonly uint Job;
[Key(9)]
public readonly uint CurrentHp;
[Key(10)]
public readonly uint MaxHp;
[Key(11)]
public readonly ulong? PartyId;
public PlayerInfo(
byte[] hash,
uint world,
float x,
float y,
float z,
float w,
byte[] customize,
byte level,
uint job,
uint currentHp,
uint maxHp,
ulong? partyId
) {
this.Hash = hash;
this.World = world;
this.X = x;
this.Y = y;
this.Z = z;
this.W = w;
this.Customize = customize;
this.Level = level;
this.Job = job;
this.CurrentHp = currentHp;
this.MaxHp = maxHp;
this.PartyId = partyId;
}
};