Files

253 lines
9.3 KiB
C#
Raw Permalink Normal View History

2026-03-16 21:38:49 +01:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Newtonsoft.Json;
namespace Guildlib.Runtime
{
// ── Wire types ────────────────────────────────────────────────────────────
public class ManifestRow
{
public string id;
public long version;
public string checksum;
}
public class ShardManifest
{
public string shard;
public List<ManifestRow> rows;
}
public class PullRequest
{
public string shard;
public List<string> ids;
// Optional platform filter — only pull rows tagged for these platforms.
// The server will include rows tagged "all" regardless of this list.
// Set to null to pull everything.
public List<string> filterPlatforms;
// Optional tag filter — only pull rows that have at least one of these tags.
// Set to null to pull everything.
public List<string> filterTags;
}
public class PullResponse
{
public string shard;
public List<Dictionary<string,object>> rows;
}
public class PushPayload
{
public string shard;
public string typeName;
public Dictionary<string,object> data;
}
public class SyncResult
{
public string shard;
public int added;
public int updated;
public int skipped;
}
// ── ShardClient ───────────────────────────────────────────────────────────
public class ShardClient
{
readonly GuildlibConfig config;
readonly HttpClient http;
readonly string shardName;
readonly string dbPath;
// Local manifest: entryId -> (version, checksum)
Dictionary<string,(long version, string checksum)> localManifest = new();
public ShardClient(GuildlibConfig cfg, string shard)
{
config = cfg;
shardName = shard;
http = new HttpClient();
if (!string.IsNullOrEmpty(cfg.apiKey))
http.DefaultRequestHeaders.Add("X-Api-Key", cfg.apiKey);
var dir = Path.Combine(Application.persistentDataPath, cfg.shardFolder);
Directory.CreateDirectory(dir);
dbPath = Path.Combine(dir, $"{shard}.sqlite");
LoadLocalManifest();
}
// ── Pull ─────────────────────────────────────────────────────────────
/// <summary>
/// Pull all changed rows from the server for this shard.
///
/// platformFilter: if non-null, only pull rows whose platforms include
/// at least one of the given values, or "all".
/// Example: new List<string>{"pc","ps5"}
///
/// tagFilter: if non-null, only pull rows with at least one matching tag.
/// Example: new List<string>{"release","base_game"}
///
/// Both filters are applied server-side so no extra data crosses the wire.
/// </summary>
public async Task<SyncResult> PullAsync(
List<string> platformFilter = null,
List<string> tagFilter = null)
{
// 1 — Fetch server manifest (lightweight: id/version/checksum only)
var manifestJson = await http.GetStringAsync($"{config.serverUrl}/manifest/{shardName}");
var serverManifest = JsonConvert.DeserializeObject<ShardManifest>(manifestJson);
// 2 — Diff against local manifest
var needIds = new List<string>();
foreach (var row in serverManifest.rows)
{
if (!localManifest.TryGetValue(row.id, out var local) ||
local.version < row.version ||
local.checksum != row.checksum)
needIds.Add(row.id);
}
var result = new SyncResult
{
shard = shardName,
skipped = serverManifest.rows.Count - needIds.Count
};
if (needIds.Count == 0) return result;
// 3 — Request only the delta rows, with optional server-side filtering
var pullReq = new PullRequest
{
shard = shardName,
ids = needIds,
filterPlatforms = platformFilter,
filterTags = tagFilter,
};
var reqJson = JsonConvert.SerializeObject(pullReq);
var response = await http.PostAsync(
$"{config.serverUrl}/pull",
new StringContent(reqJson, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
var pullJson = await response.Content.ReadAsStringAsync();
var pullResp = JsonConvert.DeserializeObject<PullResponse>(pullJson);
// 4 — Upsert into local SQLite shard
int added = 0, updated = 0;
using var db = new LocalShardDb(dbPath);
db.EnsureTable(shardName);
foreach (var row in pullResp.rows)
{
bool isNew = !localManifest.ContainsKey(row["id"].ToString());
db.Upsert(shardName, row);
if (isNew) added++; else updated++;
}
// 5 — Persist updated manifest
foreach (var mr in serverManifest.rows)
localManifest[mr.id] = (mr.version, mr.checksum);
SaveLocalManifest();
result.added = added;
result.updated = updated;
return result;
}
// ── Push ─────────────────────────────────────────────────────────────
public async Task PushAsync(DataEntry entry)
{
var data = EntryToDict(entry);
var payload = new PushPayload
{
shard = shardName,
typeName = entry.typeName,
data = data,
};
var json = JsonConvert.SerializeObject(payload);
var response = await http.PostAsync(
$"{config.serverUrl}/push",
new StringContent(json, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
}
// ── Local manifest ────────────────────────────────────────────────────
void LoadLocalManifest()
{
var path = dbPath + ".manifest.json";
if (!File.Exists(path)) return;
try
{
var raw = File.ReadAllText(path);
localManifest = JsonConvert.DeserializeObject<
Dictionary<string,(long, string)>>(raw) ?? new();
}
catch { localManifest = new(); }
}
void SaveLocalManifest()
{
File.WriteAllText(dbPath + ".manifest.json",
JsonConvert.SerializeObject(localManifest, Formatting.Indented));
}
// ── Helpers ───────────────────────────────────────────────────────────
static Dictionary<string,object> EntryToDict(DataEntry entry)
{
var dict = new Dictionary<string,object>();
foreach (var f in entry.GetType().GetFields(
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance))
{
dict[f.Name] = f.GetValue(entry);
}
return dict;
}
}
// ── LocalShardDb stub ─────────────────────────────────────────────────────
// Replace the method bodies with your chosen Unity SQLite package.
// Recommended: https://github.com/gkoreman/SQLite4Unity3d
// or sqlite-net-pcl via NuGet
public class LocalShardDb : IDisposable
{
readonly string path;
public LocalShardDb(string dbPath) { path = dbPath; }
public void EnsureTable(string tableName)
{
// CREATE TABLE IF NOT EXISTS <tableName> (
// id TEXT PRIMARY KEY,
// type_name TEXT,
// parent_id TEXT,
// platforms TEXT, -- JSON array
// tags TEXT, -- JSON array
// row_version INTEGER,
// data_json TEXT,
// updated_at INTEGER
// )
}
public void Upsert(string tableName, Dictionary<string,object> row)
{
// INSERT OR REPLACE INTO <tableName> VALUES (...)
}
public void Dispose() { }
}
}