From b6569a7787d94b4f7090858012c819d250a6996d Mon Sep 17 00:00:00 2001 From: XxKami1337 <159930006+XxKami1337@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:01:56 +0100 Subject: [PATCH 1/2] OSHABERI Farm Implementation Adds the API For OSHABERI Farm now it Can Correctly Create and Edit Save Data There are Some Announce End Points Missing this is Due to Missing Scene SDATs So I Don't know what they are so Cannot Add Them --- .../GameServices/PSHOME/OSHABERI/Announce.cs | 55 ++++ .../GameServices/PSHOME/OSHABERI/Campaign.cs | 42 +++ .../PSHOME/OSHABERI/EventRecords.cs | 207 ++++++++++++++ .../PSHOME/OSHABERI/GlobalSetting.cs | 42 +++ .../PSHOME/OSHABERI/OSHABERIClass.cs | 83 ++++++ .../GameServices/PSHOME/OSHABERI/UserData.cs | 256 ++++++++++++++++++ .../RouteHandlers/GameRoutes/WebAPIRoutes.cs | 36 +++ Servers/ApacheNet/Program.cs | 3 +- 8 files changed, 723 insertions(+), 1 deletion(-) create mode 100644 AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Announce.cs create mode 100644 AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Campaign.cs create mode 100644 AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/EventRecords.cs create mode 100644 AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/GlobalSetting.cs create mode 100644 AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/OSHABERIClass.cs create mode 100644 AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/UserData.cs diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Announce.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Announce.cs new file mode 100644 index 00000000..9ef8180d --- /dev/null +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Announce.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Text; +using CustomLogger; + +namespace WebAPIService.GameServices.PSHOME.OSHABERI +{ + public class Announce + { + private const string HardcodedDefault = + "{" + + "\"ver_\":1," + + "\"updateTime_\":300," + + "\"infoLifeTime_\":10," + + "\"announces_\":[]" + + "}"; + + public static string loadEntrygate(string workPath, string fulluripath) + { + return Serve(workPath, "Announce_entrygate.json", "entrygate"); + } + + public static string loadFarm(string workPath, string fulluripath) + { + return Serve(workPath, "Announce_farm.json", "farm"); + } + + public static string loadGarden(string workPath, string fulluripath) + { + return Serve(workPath, "Announce_garden.json", "garden"); + } + + private static string Serve(string workPath, string fileName, string sceneName) + { + string overridePath = Path.Combine(workPath, "oshaberi", "config", fileName); + + if (File.Exists(overridePath)) + { + try + { + string json = File.ReadAllText(overridePath, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - Announce ({sceneName}): serving override ({json.Length} bytes)"); + return json; + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"[OSHABERI] - Announce ({sceneName}): override read failed, using default: {ex}"); + } + } + + LoggerAccessor.LogInfo($"[OSHABERI] - Announce ({sceneName}): serving empty feed"); + return HardcodedDefault; + } + } +} \ No newline at end of file diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Campaign.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Campaign.cs new file mode 100644 index 00000000..ff91ce0a --- /dev/null +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/Campaign.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Text; +using CustomLogger; + +namespace WebAPIService.GameServices.PSHOME.OSHABERI +{ + public class Campaign + { + private const string HardcodedDefault = + "{" + + "\"ver_\":1," + + "\"updateTime_\":300," + + "\"conditionCheckTime_\":60," + + "\"infoLifeTime_\":10," + + "\"campaigns_\":[]," + + "\"events_\":[]" + + "}"; + + public static string loadCampaign(string workPath, string fulluripath) + { + string overridePath = Path.Combine(workPath, "oshaberi", "config", "Campaign.json"); + + if (File.Exists(overridePath)) + { + try + { + string json = File.ReadAllText(overridePath, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - Campaign: serving override ({json.Length} bytes)"); + return json; + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"[OSHABERI] - Campaign: override read failed, using defaults: {ex}"); + } + } + + LoggerAccessor.LogInfo("[OSHABERI] - Campaign: serving hardcoded default (no active campaigns)"); + return HardcodedDefault; + } + } +} \ No newline at end of file diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/EventRecords.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/EventRecords.cs new file mode 100644 index 00000000..ffcd4573 --- /dev/null +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/EventRecords.cs @@ -0,0 +1,207 @@ +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using MultiServerLibrary.HTTP; +using CustomLogger; +using HttpMultipartParser; + + + + +#if !NETFRAMEWORK +using System.Web; +#endif + +namespace WebAPIService.GameServices.PSHOME.OSHABERI +{ + public class EventRecords + { + private const string DefaultEventJson = + "{\"ver_\":1,\"records_\":{}}"; + + public static string loadUserEvent(byte[] PostData, string ContentType, string workPath, string fulluripath, string method) + { + string userId = string.Empty; + + try + { + if (method == "GET" || ContentType == null) + { +#if NETFRAMEWORK + userId = HTTPProcessor.GetQueryParameters(fulluripath)["u"]; +#else + userId = HttpUtility.ParseQueryString(new Uri("http://x" + fulluripath).Query).Get("u"); +#endif + } + else + { + string boundary = HTTPProcessor.ExtractBoundary(ContentType); + using (MemoryStream ms = new MemoryStream(PostData)) + { + userId = MultipartFormDataParser.Parse(ms, boundary).GetParameterValue("u") ?? string.Empty; + ms.Flush(); + } + } + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[OSHABERI] - loadUserEvent: parse error: {ex}"); + return DefaultEventJson; + } + + if (string.IsNullOrEmpty(userId)) + { + LoggerAccessor.LogWarn("[OSHABERI] - loadUserEvent: no userId, returning default"); + return DefaultEventJson; + } + + string safeId = SanitiseUserId(userId); + string evDir = Path.Combine(workPath, "oshaberi", "events"); + Directory.CreateDirectory(evDir); + string filePath = Path.Combine(evDir, safeId + ".json"); + + if (File.Exists(filePath)) + { + string json = File.ReadAllText(filePath, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - loadUserEvent: loaded for '{userId}' ({json.Length} bytes)"); + return json; + } + + File.WriteAllText(filePath, DefaultEventJson, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - loadUserEvent: created default for new user '{userId}'"); + return DefaultEventJson; + } + + public static string saveUserEvent(byte[] PostData, string ContentType, string workPath) + { + if (PostData == null || PostData.Length == 0) + { + LoggerAccessor.LogError("[OSHABERI] - saveUserEvent: empty POST body"); + return ErrorJson(-1); + } + + string userId = string.Empty; + string recordsJson = string.Empty; + + try + { + string boundary = HTTPProcessor.ExtractBoundary(ContentType); + using (MemoryStream ms = new MemoryStream(PostData)) + { + var form = MultipartFormDataParser.Parse(ms, boundary); + userId = form.GetParameterValue("u") ?? string.Empty; + recordsJson = form.GetParameterValue("records") ?? string.Empty; + ms.Flush(); + } + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[OSHABERI] - saveUserEvent: parse error: {ex}"); + return ErrorJson(-1); + } + + if (string.IsNullOrEmpty(userId)) + { + LoggerAccessor.LogError("[OSHABERI] - saveUserEvent: missing userId"); + return ErrorJson(-2); + } + + if (string.IsNullOrEmpty(recordsJson) || recordsJson == "{}") + { + LoggerAccessor.LogInfo($"[OSHABERI] - saveUserEvent: empty records for '{userId}', no-op"); + return SuccessJson(); + } + + string safeId = SanitiseUserId(userId); + string evDir = Path.Combine(workPath, "oshaberi", "events"); + Directory.CreateDirectory(evDir); + string filePath = Path.Combine(evDir, safeId + ".json"); + + try + { + string existing = File.Exists(filePath) + ? File.ReadAllText(filePath, Encoding.UTF8) + : DefaultEventJson; + string merged = MergeEventRecords(existing, recordsJson); + File.WriteAllText(filePath, merged, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - saveUserEvent: saved for '{userId}' ({merged.Length} bytes)"); + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[OSHABERI] - saveUserEvent: write error: {ex}"); + return ErrorJson(-3); + } + + return SuccessJson(); + } + + private static string MergeEventRecords(string existing, string incoming) + { + try + { + using var incomingDoc = JsonDocument.Parse(incoming); + if (incomingDoc.RootElement.TryGetProperty("ver_", out _)) + return incoming; + + using var existingDoc = JsonDocument.Parse(existing); + + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + { + writer.WriteStartObject(); + + foreach (var prop in existingDoc.RootElement.EnumerateObject()) + { + if (prop.Name == "records_") + continue; + prop.WriteTo(writer); + } + + writer.WritePropertyName("records_"); + writer.WriteStartObject(); + + JsonElement existingRecords; + bool hasExisting = existingDoc.RootElement.TryGetProperty("records_", out existingRecords); + + if (hasExisting) + { + foreach (var rec in existingRecords.EnumerateObject()) + { + if (incomingDoc.RootElement.TryGetProperty(rec.Name, out _)) + continue; + rec.WriteTo(writer); + } + } + + foreach (var rec in incomingDoc.RootElement.EnumerateObject()) + rec.WriteTo(writer); + + writer.WriteEndObject(); + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(stream.ToArray()); + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"[OSHABERI] - MergeEventRecords failed, using incoming: {ex.Message}"); + return incoming; + } + } + + private static string SanitiseUserId(string userId) + { + var sb = new StringBuilder(userId.Length); + foreach (char c in userId) + sb.Append(char.IsLetterOrDigit(c) || c == '_' || c == '-' || c == '.' ? c : '_'); + return sb.Length > 0 ? sb.ToString() : "unknown"; + } + + private static string SuccessJson() => + "{\"result\":1,\"error_no\":0}"; + + private static string ErrorJson(int code) => + $"{{\"result\":0,\"error_no\":{code}}}"; + } +} \ No newline at end of file diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/GlobalSetting.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/GlobalSetting.cs new file mode 100644 index 00000000..27c00a81 --- /dev/null +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/GlobalSetting.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Text; +using CustomLogger; + +namespace WebAPIService.GameServices.PSHOME.OSHABERI +{ + public class GlobalSetting + { + private const string HardcodedDefault = + "{" + + "\"releaseVersion_\":0," + + "\"gcStep_\":2," + + "\"asignFarmReqTime_\":1," + + "\"asignFarmAnsTime_\":1," + + "\"asignFarmKeepTime_\":10," + + "\"uniqueFarmInstance_\":true" + + "}"; + + public static string loadOption(string workPath) + { + string overridePath = Path.Combine(workPath, "oshaberi", "config", "GlobalSetting.json"); + + if (File.Exists(overridePath)) + { + try + { + string json = File.ReadAllText(overridePath, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - GlobalSetting: serving override ({json.Length} bytes)"); + return json; + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"[OSHABERI] - GlobalSetting: override read failed, using defaults: {ex}"); + } + } + + LoggerAccessor.LogInfo("[OSHABERI] - GlobalSetting: serving hardcoded defaults"); + return HardcodedDefault; + } + } +} \ No newline at end of file diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/OSHABERIClass.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/OSHABERIClass.cs new file mode 100644 index 00000000..cf2dc809 --- /dev/null +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/OSHABERIClass.cs @@ -0,0 +1,83 @@ +using CustomLogger; +using System; + +namespace WebAPIService.GameServices.PSHOME.OSHABERI +{ + public class OSHABERIClass + { + private readonly string workpath; + private readonly string absolutepath; + private readonly string fulluripath; + private readonly string method; + + public OSHABERIClass(string method, string absolutepath, string workpath, string fulluripath) + { + this.method = method; + this.absolutepath = absolutepath; + this.workpath = workpath; + this.fulluripath = fulluripath; + } + + public string ProcessRequest(byte[] PostData, string ContentType) + { + if (string.IsNullOrEmpty(absolutepath)) + return null; + + PostData ??= Array.Empty(); + + switch (method) + { + case "GET": + switch (absolutepath) + { + case "/game/app/announce/load_entrygate.php": + return Announce.loadEntrygate(workpath, fulluripath); + + case "/game/app/announce/load_farm.php": + return Announce.loadFarm(workpath, fulluripath); + + case "/game/app/announce/load_garden.php": + return Announce.loadGarden(workpath, fulluripath); + + case "/game/app/userdata/load_userdata.php": + return UserData.loadUserData(PostData, ContentType, workpath, fulluripath, method); + + case "/game/app/option/load_option.php": + return GlobalSetting.loadOption(workpath); + + case "/game/app/campaign/load_campaign.php": + return Campaign.loadCampaign(workpath, fulluripath); + + case "/game/app/event/load_user_event.php": + return EventRecords.loadUserEvent(PostData, ContentType, workpath, fulluripath, method); + + default: + LoggerAccessor.LogWarn($"[OSHABERI] - Unknown GET request: {absolutepath}"); + break; + } + break; + + case "POST": + switch (absolutepath) + { + case "/game/app/userdata/save_userdata.php": + return UserData.saveUserData(PostData, ContentType, workpath); + + case "/game/app/event/save_user_event.php": + return EventRecords.saveUserEvent(PostData, ContentType, workpath); + + default: + LoggerAccessor.LogWarn($"[OSHABERI] - Unknown POST request: {absolutepath}"); + break; + } + break; + + default: + LoggerAccessor.LogWarn($"[OSHABERI] - Unsupported HTTP method: {method}"); + break; + } + + return null; + } + } +} \ No newline at end of file diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/UserData.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/UserData.cs new file mode 100644 index 00000000..9b6761ec --- /dev/null +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OSHABERI/UserData.cs @@ -0,0 +1,256 @@ +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using MultiServerLibrary.HTTP; +using CustomLogger; +using HttpMultipartParser; + + + +#if !NETFRAMEWORK +using System.Web; +#endif + +namespace WebAPIService.GameServices.PSHOME.OSHABERI +{ + public class UserData + { + private const string DefaultUserDataJson = + "{" + + "\"HeaderData\":{\"version_\":8}," + + "\"PersonalData\":{" + + "\"deliverFarmPoint_\":0," + + "\"farmPoint_\":0," + + "\"wtPotValue_\":0," + + "\"wtPotLevel_\":1," + + "\"helpPoint_\":0," + + "\"lastLoginYMD_\":0," + + "\"lastLoginHMS_\":0," + + "\"lastBonusYMD_\":0," + + "\"lastBonusHMS_\":0," + + "\"lastCoopFarmYMD_\":0," + + "\"lastCoopFarmHMS_\":0," + + "\"commonFlag_\":\"00000000\"," + + "\"tutorialFlag_\":\"00000000\"" + + "}," + + "\"OperateCropData\":{" + + "\"managedID_\":0," + + "\"exp_\":0," + + "\"level_\":1," + + "\"rankDivide_\":0," + + "\"mutatiln_\":0" + + "}," + + "\"CropStockData\":{\"crops_\":{}}," + + "\"CropPlaceData\":{\"sceneSets_\":{}}," + + "\"CropBagData\":{\"expantion_\":0,\"sortKind_\":1}," + + "\"ItemData\":{\"items_\":{}}," + + "\"CollectionData\":{\"resultBits_\":[\"0000000000000000\",\"0000000000000000\"]}" + + "}"; + + public static string loadUserData(byte[] PostData, string ContentType, string workPath, string fulluripath, string method) + { + string userId = string.Empty; + + try + { + if (method == "GET" || ContentType == null) + { +#if NETFRAMEWORK + userId = HTTPProcessor.GetQueryParameters(fulluripath)["u"]; +#else + userId = HttpUtility.ParseQueryString(new Uri("http://x" + fulluripath).Query).Get("u"); +#endif + } + else + { + string boundary = HTTPProcessor.ExtractBoundary(ContentType); + using (MemoryStream ms = new MemoryStream(PostData)) + { + userId = MultipartFormDataParser.Parse(ms, boundary).GetParameterValue("u") ?? string.Empty; + ms.Flush(); + } + } + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[OSHABERI] - loadUserData: failed to parse userId: {ex}"); + return ErrorJson(-1); + } + + if (string.IsNullOrEmpty(userId)) + { + LoggerAccessor.LogError("[OSHABERI] - loadUserData: userId is empty"); + return ErrorJson(-2); + } + + string safeId = SanitiseUserId(userId); + string userDir = Path.Combine(workPath, "oshaberi", "userdata"); + Directory.CreateDirectory(userDir); + string filePath = Path.Combine(userDir, safeId + ".json"); + + string json; + if (File.Exists(filePath)) + { + json = File.ReadAllText(filePath, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - loadUserData: loaded for '{userId}' ({json.Length} bytes)"); + } + else + { + json = DefaultUserDataJson; + File.WriteAllText(filePath, json, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - loadUserData: created default data for new user '{userId}'"); + } + + return json; + } + public static string saveUserData(byte[] PostData, string ContentType, string workPath) + { + if (PostData == null || PostData.Length == 0) + { + LoggerAccessor.LogError("[OSHABERI] - saveUserData: empty POST body"); + return ErrorJson(-1); + } + + string userId = string.Empty; + string dataJson = string.Empty; + string histJson = string.Empty; + string evJson = string.Empty; + + try + { + string boundary = HTTPProcessor.ExtractBoundary(ContentType); + using (MemoryStream ms = new MemoryStream(PostData)) + { + var form = MultipartFormDataParser.Parse(ms, boundary); + userId = form.GetParameterValue("u") ?? string.Empty; + dataJson = form.GetParameterValue("data") ?? string.Empty; + histJson = form.GetParameterValue("history") ?? string.Empty; + evJson = form.GetParameterValue("event") ?? string.Empty; + ms.Flush(); + } + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[OSHABERI] - saveUserData: failed to parse multipart: {ex}"); + return ErrorJson(-1); + } + + if (string.IsNullOrEmpty(userId)) + { + LoggerAccessor.LogError("[OSHABERI] - saveUserData: userId field missing"); + return ErrorJson(-2); + } + + if (string.IsNullOrEmpty(dataJson) || dataJson == "{}" || dataJson == "[]") + { + LoggerAccessor.LogInfo($"[OSHABERI] - saveUserData: empty delta for '{userId}', no-op"); + return SuccessJson(); + } + + string safeId = SanitiseUserId(userId); + + string userDir = Path.Combine(workPath, "oshaberi", "userdata"); + Directory.CreateDirectory(userDir); + string filePath = Path.Combine(userDir, safeId + ".json"); + + try + { + string existing = File.Exists(filePath) + ? File.ReadAllText(filePath, Encoding.UTF8) + : DefaultUserDataJson; + + string merged = MergeJsonObjects(existing, dataJson); + File.WriteAllText(filePath, merged, Encoding.UTF8); + LoggerAccessor.LogInfo($"[OSHABERI] - saveUserData: saved for '{userId}' ({merged.Length} bytes)"); + } + catch (Exception ex) + { + LoggerAccessor.LogError($"[OSHABERI] - saveUserData: write error: {ex}"); + return ErrorJson(-3); + } + + if (!string.IsNullOrEmpty(histJson) && histJson != "{}" && histJson != "[]") + { + try + { + string histDir = Path.Combine(workPath, "oshaberi", "history"); + Directory.CreateDirectory(histDir); + string histFile = Path.Combine(histDir, safeId + ".jsonl"); + File.AppendAllText(histFile, $"{{\"ts\":\"{DateTime.UtcNow:o}\",\"data\":{histJson}}}\n", Encoding.UTF8); + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"[OSHABERI] - saveUserData: history write failed for '{userId}': {ex}"); + } + } + + if (!string.IsNullOrEmpty(evJson) && evJson != "{}" && evJson != "[]") + { + try + { + string evDir = Path.Combine(workPath, "oshaberi", "eventdiff"); + Directory.CreateDirectory(evDir); + string evFile = Path.Combine(evDir, safeId + ".json"); + string existingEv = File.Exists(evFile) + ? File.ReadAllText(evFile, Encoding.UTF8) + : "{}"; + File.WriteAllText(evFile, MergeJsonObjects(existingEv, evJson), Encoding.UTF8); + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"[OSHABERI] - saveUserData: event diff write failed for '{userId}': {ex}"); + } + } + + return SuccessJson(); + } + private static string MergeJsonObjects(string baseJson, string patchJson) + { + try + { + using var baseDoc = JsonDocument.Parse(baseJson); + using var patchDoc = JsonDocument.Parse(patchJson); + + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { SkipValidation = false })) + { + writer.WriteStartObject(); + + foreach (var prop in baseDoc.RootElement.EnumerateObject()) + { + if (patchDoc.RootElement.TryGetProperty(prop.Name, out _)) + continue; + prop.WriteTo(writer); + } + + foreach (var prop in patchDoc.RootElement.EnumerateObject()) + prop.WriteTo(writer); + + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(stream.ToArray()); + } + catch (Exception ex) + { + LoggerAccessor.LogWarn($"[OSHABERI] - MergeJsonObjects parse failed, using patch directly: {ex.Message}"); + return patchJson; + } + } + + private static string SanitiseUserId(string userId) + { + var sb = new StringBuilder(userId.Length); + foreach (char c in userId) + sb.Append(char.IsLetterOrDigit(c) || c == '_' || c == '-' || c == '.' ? c : '_'); + return sb.Length > 0 ? sb.ToString() : "unknown"; + } + + private static string SuccessJson() => + "{\"result\":1,\"error_no\":0}"; + + private static string ErrorJson(int code) => + $"{{\"result\":0,\"error_no\":{code}}}"; + } +} \ No newline at end of file diff --git a/Servers/ApacheNet/BuildIn/RouteHandlers/GameRoutes/WebAPIRoutes.cs b/Servers/ApacheNet/BuildIn/RouteHandlers/GameRoutes/WebAPIRoutes.cs index 88bbd18d..955d424c 100644 --- a/Servers/ApacheNet/BuildIn/RouteHandlers/GameRoutes/WebAPIRoutes.cs +++ b/Servers/ApacheNet/BuildIn/RouteHandlers/GameRoutes/WebAPIRoutes.cs @@ -29,6 +29,7 @@ using WebAPIService.GameServices.PSHOME.LOOT; using WebAPIService.GameServices.PSHOME.NDREAMS; using WebAPIService.GameServices.PSHOME.OHS; +using WebAPIService.GameServices.PSHOME.OSHABERI; using WebAPIService.GameServices.PSHOME.OUWF; using WebAPIService.GameServices.PSHOME.PREMIUMAGENCY; using WebAPIService.GameServices.PSHOME.RCHOME; @@ -876,6 +877,41 @@ internal class WebAPIRoutes return ctx.Response.Send(res).Result; } }, + new() { + Name = "OSHABERI Farm API", + UrlRegex = @".*/game/app/.*", + Hosts = new[] { + "homect-scej.jp", + "qa-homect-scej.jp", + "oc.homect-scej.jp" + }, + Callable = (ctx) => { + HttpStatusCode statusCode; + ctx.Response.ChunkedTransfer = ctx.AcceptChunked; + + string? res = new OSHABERIClass(ctx.Request.Method.ToString(), ctx.AbsolutePath, ApacheNetServerConfiguration.APIStaticFolder, ctx.FullUrl).ProcessRequest(ctx.Request.DataAsBytes, ctx.Request.ContentType); + if (string.IsNullOrEmpty(res)) + { + ctx.Response.ContentType = "text/plain"; + statusCode = HttpStatusCode.InternalServerError; + } + else + { + ctx.Response.Headers.Add("Date", DateTime.Now.ToString("r")); + ctx.Response.ContentType = "application/json"; + statusCode = HttpStatusCode.OK; + } + ctx.Response.StatusCode = (int)statusCode; + byte[]? responseBytes = !string.IsNullOrEmpty(res) + ? new UTF8Encoding(false).GetBytes(res) + : null; + + if (ctx.Response.ChunkedTransfer) + return ctx.Response.SendChunk(responseBytes, true).Result; + else + return ctx.Response.Send(responseBytes).Result; + } + }, new() { Name = "FROMSOFTWARE API", Hosts = new[] { "acvd-ps3ww-cdn.fromsoftware.jp" }, diff --git a/Servers/ApacheNet/Program.cs b/Servers/ApacheNet/Program.cs index 3fb2afee..2506be1d 100644 --- a/Servers/ApacheNet/Program.cs +++ b/Servers/ApacheNet/Program.cs @@ -126,7 +126,8 @@ public static class ApacheNetServerConfiguration "secure.cpreprod.homeps3.online.scee.com", "secure.heavyh2o.net", "game.hellfiregames.com", - "www.ndreamsgateway.com" + "www.ndreamsgateway.com", + "oc.homect-scej.jp" }; public static List? Ports { get; set; } = new() { NetworkPorts.Http.Tcp, NetworkPorts.Http.Ssl, 3074, 3658, 9090, 10010, 26004, 33000 }; public static List? RedirectRules { get; set; } From a508777d08a5e413b9c1860cca995444a1680692 Mon Sep 17 00:00:00 2001 From: XxKami1337 <159930006+XxKami1337@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:37:26 +0100 Subject: [PATCH 2/2] Add CONTROLS For Uproar + Fix VeeMee MetaScores + Screens --- .../GameServices/PSHOME/OHS/User.cs | 5 + .../GameServices/PSHOME/VEEMEE/VEEMEEClass.cs | 25 +- .../PSHOME/VEEMEE/meta/MetaScores.cs | 295 ++++++++++++------ 3 files changed, 230 insertions(+), 95 deletions(-) diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OHS/User.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OHS/User.cs index e11ce4a3..52571532 100644 --- a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OHS/User.cs +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/OHS/User.cs @@ -541,6 +541,11 @@ public static string Get(byte[] PostData, string ContentType, string directorypa if (directorypath.Contains("casino")) output = "nil"; break; + case "CONTROLS": + if (directorypath.Contains("EmergencyState")) + output = "{ [\"VIBRATION\"] = true, [\"AIM_INVERT\"] = false, " + + "[\"AIM_SENSITIVITY\"] = 0.5, [\"TURN_SENSITIVITY\"] = 0.5 }"; + break; case "GameState": if (directorypath.Contains("shooter_game")) output = "{ [\"currentLevel\"] = 1, [\"currentMaxLevel\"] = 50, [\"items\"] = {\t{ type = \"guns\" \t\t , name=\"repeater\"\t\t\t, level=1 , inUse = false }\r\n" + diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/VEEMEEClass.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/VEEMEEClass.cs index 4f02f698..5cff399b 100644 --- a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/VEEMEEClass.cs +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/VEEMEEClass.cs @@ -1,5 +1,6 @@ using MultiServerLibrary.HTTP; using System.Text; +using System.IO; using WebAPIService.GameServices.PSHOME.VEEMEE.accorn; using WebAPIService.GameServices.PSHOME.VEEMEE.audi_sled; using WebAPIService.GameServices.PSHOME.VEEMEE.audi_vrun; @@ -93,11 +94,11 @@ public VEEMEEClass(string method, string absolutepath) resultContentType = "text/xml"; break; case "/MetaScores/setScore.php": - result = MetaScores.SetUserDataPOST(postData, contentType, apiPath); + result = MetaScores.SetUserDataPOST(postData, HTTPProcessor.ExtractBoundary(contentType), apiPath); resultContentType = "text/xml"; break; case "/MetaScores/getScore.php": - result = MetaScores.GetUserDataPOST(postData, contentType, apiPath); + result = MetaScores.GetUserDataPOST(postData, HTTPProcessor.ExtractBoundary(contentType), apiPath); resultContentType = "text/xml"; break; case "/MetaScores/getTrophies.php": @@ -109,11 +110,19 @@ public VEEMEEClass(string method, string absolutepath) resultContentType = "text/xml"; break; case "/MetaScores/getHighScores.php": + result = MetaScores.GetHighScoresPOST(postData, HTTPProcessor.ExtractBoundary(contentType), apiPath, filter: 0, friends: false); + resultContentType = "text/xml"; + break; case "/MetaScores/getHighScoresToday.php": + result = MetaScores.GetHighScoresPOST(postData, HTTPProcessor.ExtractBoundary(contentType), apiPath, filter: 1, friends: false); + resultContentType = "text/xml"; + break; case "/MetaScores/getHighScoresYesterday.php": + result = MetaScores.GetHighScoresPOST(postData, HTTPProcessor.ExtractBoundary(contentType), apiPath, filter: 2, friends: false); + resultContentType = "text/xml"; + break; case "/MetaScores/getHighScoresFriends.php": - // TODO: Implements MetaScores dynamic leaderboards! - result = ""; + result = MetaScores.GetHighScoresPOST(postData, HTTPProcessor.ExtractBoundary(contentType), apiPath, filter: 0, friends: true); resultContentType = "text/xml"; break; case "/player_profiles/setPlayerProfile.php": @@ -305,6 +314,14 @@ public VEEMEEClass(string method, string absolutepath) case "/stats/getconfig.php": result = Stats.GetConfig(true, postData, contentType, apiPath); break; + case "/screens.php": + string screensPath = $"{apiPath}/VEEMEE/screens/screens.xml"; // Just a Big Screenlinks File + if (File.Exists(screensPath)) + result = File.ReadAllText(screensPath); + else + result = ""; + resultContentType = "text/xml"; + break; default: break; } diff --git a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/meta/MetaScores.cs b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/meta/MetaScores.cs index 0c5cf391..45382103 100644 --- a/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/meta/MetaScores.cs +++ b/AuxiliaryServices/WebAPIService/GameServices/PSHOME/VEEMEE/meta/MetaScores.cs @@ -1,125 +1,238 @@ +using HttpMultipartParser; +using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; -using System.Xml; -using MultiServerLibrary.HTTP; +using System.Text; namespace WebAPIService.GameServices.PSHOME.VEEMEE.meta { public class MetaScores { - public static string SetUserDataPOST(byte[] PostData, string ContentType, string apiPath) - { - string key = string.Empty; - string psnid = string.Empty; - string game_id = string.Empty; - string sort_1 = string.Empty; - string sort_2 = string.Empty; - string score_1 = string.Empty; - string score_2 = string.Empty; - - if (ContentType == "application/x-www-form-urlencoded" && PostData != null) - { - var data = HTTPProcessor.ExtractAndSortUrlEncodedPOSTData(PostData); - key = data["key"].First(); - if (key != "JPDFC10A9MXS8HHOMOUKYAR3") - { - CustomLogger.LoggerAccessor.LogError("[VEEMEE] - meta_scores - Client tried to push invalid key! Invalidating request."); - return null; - } - psnid = data["psnid"].First(); - game_id = data["game_id"].First(); - sort_1 = data["sort_1"].First(); - sort_2 = data["sort_2"].First(); - score_1 = data["score_1"].First(); - score_2 = data["score_2"].First(); + private const string ValidKey = "JPDFC10A9MXS8HHOMOUKYAR3"; - string directoryPath = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_1}/User_Data"; - string directoryPath_2 = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_2}/User_Data"; - - string filePath = $"{directoryPath}/{psnid}.xml"; - string filePath_2 = $"{directoryPath}/{psnid}_2.xml"; - - Directory.CreateDirectory(directoryPath); - Directory.CreateDirectory(directoryPath_2); + public static string SetUserDataPOST(byte[] PostData, string boundary, string apiPath) + { + if (string.IsNullOrEmpty(boundary) || PostData == null) + return null; - if (File.Exists(filePath) && File.Exists(filePath_2)) + try + { + using (MemoryStream ms = new MemoryStream(PostData)) { - // Load the XML string into an XmlDocument - XmlDocument xmlDoc = new XmlDocument(); - xmlDoc.LoadXml($"{File.ReadAllText(filePath)}"); + var data = MultipartFormDataParser.Parse(ms, boundary); + + string key = data.GetParameterValue("key"); + if (key != ValidKey) + { + CustomLogger.LoggerAccessor.LogError("[VEEMEE] - meta_scores - SetUserData: invalid key."); + return null; + } + + string psnid = data.GetParameterValue("psnid"); + string game_id = data.GetParameterValue("game_id"); + string sort_1 = data.GetParameterValue("sort_1"); + string sort_2 = data.GetParameterValue("sort_2"); + string score_1 = data.GetParameterValue("score_1"); + string score_2 = data.GetParameterValue("score_2"); + + string directoryPath = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_1}/User_Data"; + string directoryPath_2 = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_2}/User_Data"; + string filePath = $"{directoryPath}/{psnid}.xml"; + string filePath_2 = $"{directoryPath_2}/{psnid}_2.xml"; + + Directory.CreateDirectory(directoryPath); + Directory.CreateDirectory(directoryPath_2); - // Find the element - XmlElement scoreElement = xmlDoc.SelectSingleNode("/xml/score") as XmlElement; - - if (scoreElement != null) - // Replace the value of with a new value - scoreElement.InnerText = score_1; - - File.WriteAllText(filePath, xmlDoc.OuterXml.Replace("", string.Empty).Replace("", string.Empty)); - - xmlDoc = new XmlDocument(); - xmlDoc.LoadXml($"{File.ReadAllText(filePath_2)}"); - - // Find the element - scoreElement = xmlDoc.SelectSingleNode("/xml/score") as XmlElement; - - if (scoreElement != null) - // Replace the value of with a new value - scoreElement.InnerText = score_2; - - File.WriteAllText(filePath, xmlDoc.OuterXml.Replace("", string.Empty).Replace("", string.Empty)); - - return $"{psnid}{score_1}{score_2}"; - } - else - { File.WriteAllText(filePath, $"{score_1}"); File.WriteAllText(filePath_2, $"{score_2}"); return $"{psnid}{score_1}{score_2}"; } } + catch (Exception ex) + { + CustomLogger.LoggerAccessor.LogError($"[VEEMEE] - meta_scores - SetUserDataPOST exception: {ex}"); + } return null; } - public static string GetUserDataPOST(byte[] PostData, string ContentType, string apiPath) + public static string GetUserDataPOST(byte[] PostData, string boundary, string apiPath) { - string key = string.Empty; - string psnid = string.Empty; - string game_id = string.Empty; - string sort_1 = string.Empty; - string sort_2 = string.Empty; + if (string.IsNullOrEmpty(boundary) || PostData == null) + return null; - if (ContentType == "application/x-www-form-urlencoded" && PostData != null) + try { - var data = HTTPProcessor.ExtractAndSortUrlEncodedPOSTData(PostData); - key = data["key"].First(); - if (key != "JPDFC10A9MXS8HHOMOUKYAR3") + using (MemoryStream ms = new MemoryStream(PostData)) { - CustomLogger.LoggerAccessor.LogError("[VEEMEE] - meta_scores - Client tried to push invalid key! Invalidating request."); - return null; + var data = MultipartFormDataParser.Parse(ms, boundary); + + string key = data.GetParameterValue("key"); + if (key != ValidKey) + { + CustomLogger.LoggerAccessor.LogError("[VEEMEE] - meta_scores - GetUserData: invalid key."); + return null; + } + + string psnid = data.GetParameterValue("psnid"); + string game_id = data.GetParameterValue("game_id"); + string sort_1 = data.GetParameterValue("sort_1"); + string sort_2 = data.GetParameterValue("sort_2"); + + string filePath = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_1}/User_Data/{psnid}.xml"; + string filePath_2 = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_2}/User_Data/{psnid}_2.xml"; + + if (File.Exists(filePath) && File.Exists(filePath_2)) + { + string score_1 = File.ReadAllText(filePath).Replace("", string.Empty).Replace("", string.Empty).Trim(); + string score_2 = File.ReadAllText(filePath_2).Replace("", string.Empty).Replace("", string.Empty).Trim(); + return $"{psnid}{score_1}{score_2}"; + } + + return $"{psnid}00"; } - psnid = data["psnid"].First(); - game_id = data["game_id"].First(); - sort_1 = data["sort_1"].First(); - sort_2 = data["sort_2"].First(); - - string directoryPath = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_1}/User_Data"; - string directoryPath_2 = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_2}/User_Data"; + } + catch (Exception ex) + { + CustomLogger.LoggerAccessor.LogError($"[VEEMEE] - meta_scores - GetUserDataPOST exception: {ex}"); + } - string filePath = $"{directoryPath}/{psnid}.xml"; - string filePath_2 = $"{directoryPath}/{psnid}_2.xml"; + return null; + } + public static string GetHighScoresPOST(byte[] PostData, string boundary, string apiPath, int filter = 0, bool friends = false) + { + if (string.IsNullOrEmpty(boundary) || PostData == null) + return ""; - if (File.Exists(filePath) && File.Exists(filePath_2)) + try + { + using (MemoryStream ms = new MemoryStream(PostData)) { - string score_1 = File.ReadAllText(filePath).Replace("", string.Empty).Replace("", string.Empty); - string score_2 = File.ReadAllText(filePath_2).Replace("", string.Empty).Replace("", string.Empty); - return $"{psnid}{score_1}{score_2}"; + var data = MultipartFormDataParser.Parse(ms, boundary); + + string key = data.GetParameterValue("key"); + if (key != ValidKey) + { + CustomLogger.LoggerAccessor.LogError("[VEEMEE] - meta_scores - GetHighScores: invalid key."); + return ""; + } + + string game_id = data.GetParameterValue("game_id"); + string sort_1 = data.GetParameterValue("sort_1"); + + if (string.IsNullOrEmpty(game_id)) + { + CustomLogger.LoggerAccessor.LogError("[VEEMEE] - meta_scores - GetHighScores: missing game_id."); + return ""; + } + + string userDataPath = $"{apiPath}/VEEMEE/meta_scores/{game_id}/{sort_1}/User_Data"; + + if (!Directory.Exists(userDataPath)) + return ""; + + HashSet friendsFilter = null; + if (friends) + { + friendsFilter = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var param in data.Parameters.Where(p => p.Name == "friends[]")) + friendsFilter.Add(param.Data); + } + + DateTime? dateFrom = null; + DateTime? dateTo = null; + if (filter == 1) // Today + { + dateFrom = DateTime.UtcNow.Date; + dateTo = dateFrom.Value.AddDays(1); + } + else if (filter == 2) + { + dateFrom = DateTime.UtcNow.Date.AddDays(-1); + dateTo = DateTime.UtcNow.Date; + } + + var entries = new List<(string psnid, long score1, long score2)>(); + + foreach (string filePath in Directory.EnumerateFiles(userDataPath, "*.xml")) + { + string fileName = Path.GetFileNameWithoutExtension(filePath); + + if (fileName.EndsWith("_2")) + continue; + + if (dateFrom.HasValue) + { + DateTime lastWrite = File.GetLastWriteTimeUtc(filePath); + if (lastWrite < dateFrom.Value || lastWrite >= dateTo.Value) + continue; + } + + if (friendsFilter != null && !friendsFilter.Contains(fileName)) + continue; + + long score1 = 0; + try + { + string raw = File.ReadAllText(filePath) + .Replace("", string.Empty) + .Replace("", string.Empty) + .Trim(); + if (double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out double d1)) + score1 = (long)d1; + } + catch { continue; } + + long score2 = 0; + string filePath_2 = Path.Combine(userDataPath, fileName + "_2.xml"); + if (File.Exists(filePath_2)) + { + try + { + string raw2 = File.ReadAllText(filePath_2) + .Replace("", string.Empty) + .Replace("", string.Empty) + .Trim(); + if (double.TryParse(raw2, NumberStyles.Float, CultureInfo.InvariantCulture, out double d2)) + score2 = (long)d2; + } + catch { } + } + + entries.Add((fileName, score1, score2)); + } + + if (sort_1.Equals("DESC", StringComparison.OrdinalIgnoreCase)) + entries = entries.OrderByDescending(e => e.score1).ToList(); + else + entries = entries.OrderBy(e => e.score1).ToList(); + + var sb = new StringBuilder(); + sb.Append(""); + int limit = Math.Min(10, entries.Count); + for (int i = 0; i < limit; i++) + { + var e = entries[i]; + sb.Append(""); + sb.Append($"{e.psnid}"); + sb.Append("0"); + sb.Append($"{e.score1}"); + sb.Append($"{e.score2}"); + sb.Append(""); + } + sb.Append(""); + return sb.ToString(); } } + catch (Exception ex) + { + CustomLogger.LoggerAccessor.LogError($"[VEEMEE] - meta_scores - GetHighScoresPOST exception: {ex}"); + } - return $"{psnid}00"; + return ""; } } -} +} \ No newline at end of file