253 lines
9.3 KiB
C#
253 lines
9.3 KiB
C#
|
|
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() { }
|
||
|
|
}
|
||
|
|
}
|