209 lines
6.0 KiB
C#
209 lines
6.0 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 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; }
|
|
|
|
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();
|
|
}
|
|
|
|
internal void SaveConfig() {
|
|
this.Interface.SavePluginConfig(this.Config);
|
|
}
|
|
|
|
private void FrameworkUpdate(IFramework framework) {
|
|
if (this.Stopwatch.Elapsed < TimeSpan.FromSeconds(this.Config.UpdateFrequency) || this.ClientState.LocalPlayer is not { } localPlayer) {
|
|
return;
|
|
}
|
|
|
|
this.Stopwatch.Restart();
|
|
|
|
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.CompanyTag.TextValue,
|
|
player.CurrentHp,
|
|
player.MaxHp
|
|
);
|
|
})
|
|
.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(2, 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 req = new HttpRequestMessage(HttpMethod.Post, "https://map.anna.lgbt/api/upload") {
|
|
Content = new ByteArrayContent(mem.ToArray()) {
|
|
Headers = {
|
|
ContentType = new MediaTypeHeaderValue("application/msgpack"),
|
|
ContentEncoding = { "gzip" },
|
|
},
|
|
},
|
|
};
|
|
|
|
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(byte version, uint territory, uint world, List<PlayerInfo> players) {
|
|
[Key(0)]
|
|
public readonly byte Version = version;
|
|
|
|
[Key(1)]
|
|
public readonly uint Territory = territory;
|
|
|
|
[Key(2)]
|
|
public readonly uint World = world;
|
|
|
|
[Key(3)]
|
|
public readonly List<PlayerInfo> Players = players;
|
|
}
|
|
|
|
[Serializable]
|
|
[MessagePackObject]
|
|
public struct PlayerInfo(
|
|
byte[] hash,
|
|
uint world,
|
|
float x,
|
|
float y,
|
|
float z,
|
|
float w,
|
|
byte[] customize,
|
|
byte level,
|
|
uint job,
|
|
string freeCompany,
|
|
uint currentHp,
|
|
uint maxHp
|
|
) {
|
|
[Key(0)]
|
|
public readonly byte[] Hash = hash;
|
|
|
|
[Key(1)]
|
|
public readonly uint World = world;
|
|
|
|
[Key(2)]
|
|
public readonly float X = x;
|
|
|
|
[Key(3)]
|
|
public readonly float Y = y;
|
|
|
|
[Key(4)]
|
|
public readonly float Z = z;
|
|
|
|
[Key(5)]
|
|
public readonly float W = w;
|
|
|
|
[Key(6)]
|
|
public readonly byte[] Customize = customize;
|
|
|
|
[Key(7)]
|
|
public readonly byte Level = level;
|
|
|
|
[Key(8)]
|
|
public readonly uint Job = job;
|
|
|
|
[Key(9)]
|
|
public readonly string FreeCompany = freeCompany;
|
|
|
|
[Key(10)]
|
|
public readonly uint CurrentHp = currentHp;
|
|
|
|
[Key(11)]
|
|
public readonly uint MaxHp = maxHp;
|
|
};
|