mirror of
https://github.com/BillyCool/MariesWonderland.git
synced 2026-03-22 06:52:24 +01:00
Implement more APIs. Optimize asset and resource loading.
This commit is contained in:
@@ -26,4 +26,10 @@ public sealed class DataOptions
|
|||||||
public string LatestMasterDataVersion { get; init; } = string.Empty;
|
public string LatestMasterDataVersion { get; init; } = string.Empty;
|
||||||
public string UserDataPath { get; init; } = string.Empty;
|
public string UserDataPath { get; init; } = string.Empty;
|
||||||
public string MasterDataPath { get; init; } = string.Empty;
|
public string MasterDataPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the JSON file used to persist user data between server restarts.
|
||||||
|
/// If relative, resolved against the application base directory.
|
||||||
|
/// </summary>
|
||||||
|
public string UserDatabase { get; init; } = "userdata.json";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,11 +83,11 @@ public static class UserDataDiffBuilder
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes only the tables that changed since the snapshot.
|
/// Computes only the tables that changed since the snapshot.
|
||||||
/// Use this for incremental API responses (e.g. SetUserName, GameStart).
|
/// Use this for incremental API responses (e.g. SetUserName).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Dictionary<string, DiffData> Delta(Dictionary<string, string> before, DarkUserMemoryDatabase db)
|
public static Dictionary<string, DiffData> Delta(Dictionary<string, string> before, DarkUserMemoryDatabase db)
|
||||||
{
|
{
|
||||||
var diff = new Dictionary<string, DiffData>();
|
Dictionary<string, DiffData> diff = [];
|
||||||
foreach (var (table, serialize) in Serializers)
|
foreach (var (table, serialize) in Serializers)
|
||||||
{
|
{
|
||||||
var afterJson = serialize(db);
|
var afterJson = serialize(db);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MariesWonderland.Models.Entities;
|
using MariesWonderland.Models.Entities;
|
||||||
using MariesWonderland.Models.Type;
|
using MariesWonderland.Models.Type;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace MariesWonderland.Data;
|
namespace MariesWonderland.Data;
|
||||||
|
|
||||||
@@ -63,6 +64,58 @@ public class UserDataStore
|
|||||||
|
|
||||||
public IReadOnlyDictionary<long, DarkUserMemoryDatabase> All => _users;
|
public IReadOnlyDictionary<long, DarkUserMemoryDatabase> All => _users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize all user data to a JSON file on disk.
|
||||||
|
/// </summary>
|
||||||
|
public void Save(string filePath)
|
||||||
|
{
|
||||||
|
UserDataSnapshot snapshot = new()
|
||||||
|
{
|
||||||
|
Users = _users,
|
||||||
|
UuidToUserId = _uuidToUserId,
|
||||||
|
Sessions = _sessions.Values.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
JsonSerializerOptions options = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
string json = JsonSerializer.Serialize(snapshot, options);
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize user data from a JSON file on disk, replacing the current in-memory state.
|
||||||
|
/// Returns the number of users loaded.
|
||||||
|
/// </summary>
|
||||||
|
public int Load(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
string json = File.ReadAllText(filePath);
|
||||||
|
UserDataSnapshot? snapshot = JsonSerializer.Deserialize<UserDataSnapshot>(json);
|
||||||
|
|
||||||
|
if (snapshot is null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
_users.Clear();
|
||||||
|
_uuidToUserId.Clear();
|
||||||
|
_sessions.Clear();
|
||||||
|
|
||||||
|
foreach (var (userId, db) in snapshot.Users)
|
||||||
|
_users[userId] = db;
|
||||||
|
|
||||||
|
foreach (var (uuid, userId) in snapshot.UuidToUserId)
|
||||||
|
_uuidToUserId[uuid] = userId;
|
||||||
|
|
||||||
|
foreach (UserSession session in snapshot.Sessions)
|
||||||
|
_sessions[session.SessionKey] = session;
|
||||||
|
|
||||||
|
return _users.Count;
|
||||||
|
}
|
||||||
|
|
||||||
private static long GenerateUserId()
|
private static long GenerateUserId()
|
||||||
{
|
{
|
||||||
// Random 19-digit positive long (range: 1e18 to long.MaxValue)
|
// Random 19-digit positive long (range: 1e18 to long.MaxValue)
|
||||||
@@ -137,3 +190,13 @@ public class UserDataStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializable snapshot of all user data for persistence.
|
||||||
|
/// </summary>
|
||||||
|
file record UserDataSnapshot
|
||||||
|
{
|
||||||
|
public Dictionary<long, DarkUserMemoryDatabase> Users { get; init; } = [];
|
||||||
|
public Dictionary<string, long> UuidToUserId { get; init; } = [];
|
||||||
|
public List<UserSession> Sessions { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MariesWonderland.Configuration;
|
using MariesWonderland.Configuration;
|
||||||
|
using MariesWonderland.Data;
|
||||||
using MariesWonderland.Http;
|
using MariesWonderland.Http;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -17,12 +18,46 @@ public static class HttpApiExtensions
|
|||||||
string assetDatabaseBasePath = options.Paths.AssetDatabase;
|
string assetDatabaseBasePath = options.Paths.AssetDatabase;
|
||||||
string masterDatabaseBasePath = options.Paths.MasterDatabase;
|
string masterDatabaseBasePath = options.Paths.MasterDatabase;
|
||||||
string resourcesBaseUrl = options.Paths.ResourcesBaseUrl;
|
string resourcesBaseUrl = options.Paths.ResourcesBaseUrl;
|
||||||
|
string userDatabasePath = Path.IsPathRooted(options.Data.UserDatabase)
|
||||||
|
? options.Data.UserDatabase
|
||||||
|
: Path.Combine(AppContext.BaseDirectory, options.Data.UserDatabase);
|
||||||
|
|
||||||
ILogger<AssetDatabase> assetLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<AssetDatabase>();
|
ILogger<AssetDatabase> assetLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<AssetDatabase>();
|
||||||
AssetDatabase assetDb = new(assetDatabaseBasePath, assetLogger);
|
AssetDatabase assetDb = new(assetDatabaseBasePath, assetLogger);
|
||||||
|
|
||||||
app.MapGet("/", () => "Marie's Wonderland is open for business :marie:");
|
app.MapGet("/", () => "Marie's Wonderland is open for business :marie:");
|
||||||
|
|
||||||
|
// Debug endpoints for manual save/load
|
||||||
|
app.MapGet("/debug/save", () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UserDataStore store = app.Services.GetRequiredService<UserDataStore>();
|
||||||
|
store.Save(userDatabasePath);
|
||||||
|
return Results.Ok($"Saved {store.All.Count} users to {userDatabasePath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Results.Problem($"Save failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/debug/load", () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UserDataStore store = app.Services.GetRequiredService<UserDataStore>();
|
||||||
|
int count = store.Load(userDatabasePath);
|
||||||
|
return count > 0
|
||||||
|
? Results.Ok($"Loaded {count} users from {userDatabasePath}")
|
||||||
|
: Results.Ok("No save file found.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Results.Problem($"Load failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ToS. Expects the version wrapped in delimiters like "###123###".
|
// ToS. Expects the version wrapped in delimiters like "###123###".
|
||||||
app.MapGet("/web/static/{languagePath}/terms/termsofuse", (string languagePath) => $"<html><head><title>Terms of Service</title></head><body><h1>Terms of Service</h1><p>Language: {languagePath}</p><p>Version: ###123###</p></body></html>");
|
app.MapGet("/web/static/{languagePath}/terms/termsofuse", (string languagePath) => $"<html><head><title>Terms of Service</title></head><body><h1>Terms of Service</h1><p>Language: {languagePath}</p><p>Version: ###123###</p></body></html>");
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ namespace MariesWonderland.Http;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves asset bundle/resource requests by parsing list.bin protobuf indexes and info.json alias maps.
|
/// Resolves asset bundle/resource requests by parsing list.bin protobuf indexes and info.json alias maps.
|
||||||
/// Tracks the last-served list.bin revision per client IP so asset requests use the matching revision.
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Asset revisions are deltas, but revision 0 is a complete superset: it contains every objectId
|
||||||
|
/// that appears across all 818 revisions (confirmed by exhaustive analysis). Later revisions carry
|
||||||
|
/// updated versions of existing assets, not new ones. <see cref="Resolve"/> therefore checks the
|
||||||
|
/// client's current revision first, then falls back to revision 0 — a single 25 MB index that
|
||||||
|
/// covers the entire asset catalogue.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AssetDatabase(string basePath, ILogger<AssetDatabase> logger)
|
public sealed class AssetDatabase(string basePath, ILogger<AssetDatabase> logger)
|
||||||
{
|
{
|
||||||
@@ -35,9 +42,21 @@ public sealed class AssetDatabase(string basePath, ILogger<AssetDatabase> logger
|
|||||||
/// Resolves an asset request to ordered file-path candidates.
|
/// Resolves an asset request to ordered file-path candidates.
|
||||||
/// Caller should try each candidate in order, validating size and MD5, and serve the first valid one.
|
/// Caller should try each candidate in order, validating size and MD5, and serve the first valid one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The client's current revision is checked first. If the objectId is not present (e.g. a
|
||||||
|
/// recent revision carries only updated entries, not the full catalogue), revision 0 is used
|
||||||
|
/// as the fallback. Revision 0 is a confirmed superset of all objectIds across every revision.
|
||||||
|
/// </remarks>
|
||||||
public IEnumerable<AssetCandidate> Resolve(string clientIp, string assetType, string objectId)
|
public IEnumerable<AssetCandidate> Resolve(string clientIp, string assetType, string objectId)
|
||||||
{
|
{
|
||||||
string revision = _clientRevisions.TryGetValue(clientIp, out string? rev) ? rev : _lastKnownRevision;
|
string revision = _clientRevisions.TryGetValue(clientIp, out string? rev) ? rev : _lastKnownRevision;
|
||||||
|
|
||||||
|
// If the objectId isn't in the client's current revision, fall back to revision 0.
|
||||||
|
// Revision 0 is a complete superset — every objectId in the game is present there.
|
||||||
|
Dictionary<string, ListBinEntry>? currentIndex = LoadListBinIndex(revision);
|
||||||
|
if ((currentIndex is null || !currentIndex.ContainsKey(objectId)) && revision != "0")
|
||||||
|
revision = "0";
|
||||||
|
|
||||||
return ResolveForRevision(revision, assetType, objectId);
|
return ResolveForRevision(revision, assetType, objectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
src/Models/Type/QuestStateType.cs
Normal file
9
src/Models/Type/QuestStateType.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace MariesWonderland.Models.Type;
|
||||||
|
|
||||||
|
public enum QuestStateType
|
||||||
|
{
|
||||||
|
UNKNOWN = 0,
|
||||||
|
ACTIVE = 1,
|
||||||
|
CLEARED = 2,
|
||||||
|
CHALLENGE = 4,
|
||||||
|
}
|
||||||
48
src/Models/Type/SystemLanguage.cs
Normal file
48
src/Models/Type/SystemLanguage.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
namespace MariesWonderland.Models.Type;
|
||||||
|
|
||||||
|
public enum SystemLanguage
|
||||||
|
{
|
||||||
|
Afrikaans = 0,
|
||||||
|
Arabic = 1,
|
||||||
|
Basque = 2,
|
||||||
|
Belarusian = 3,
|
||||||
|
Bulgarian = 4,
|
||||||
|
Catalan = 5,
|
||||||
|
Chinese = 6,
|
||||||
|
Czech = 7,
|
||||||
|
Danish = 8,
|
||||||
|
Dutch = 9,
|
||||||
|
English = 10,
|
||||||
|
Estonian = 11,
|
||||||
|
Faroese = 12,
|
||||||
|
Finnish = 13,
|
||||||
|
French = 14,
|
||||||
|
German = 15,
|
||||||
|
Greek = 16,
|
||||||
|
Hebrew = 17,
|
||||||
|
Icelandic = 19,
|
||||||
|
Indonesian = 20,
|
||||||
|
Italian = 21,
|
||||||
|
Japanese = 22,
|
||||||
|
Korean = 23,
|
||||||
|
Latvian = 24,
|
||||||
|
Lithuanian = 25,
|
||||||
|
Norwegian = 26,
|
||||||
|
Polish = 27,
|
||||||
|
Portuguese = 28,
|
||||||
|
Romanian = 29,
|
||||||
|
Russian = 30,
|
||||||
|
SerboCroatian = 31,
|
||||||
|
Slovak = 32,
|
||||||
|
Slovenian = 33,
|
||||||
|
Spanish = 34,
|
||||||
|
Swedish = 35,
|
||||||
|
Thai = 36,
|
||||||
|
Turkish = 37,
|
||||||
|
Ukrainian = 38,
|
||||||
|
Vietnamese = 39,
|
||||||
|
ChineseSimplified = 40,
|
||||||
|
ChineseTraditional = 41,
|
||||||
|
Unknown = 42,
|
||||||
|
Hungarian = 18
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
using MariesWonderland.Configuration;
|
||||||
|
using MariesWonderland.Data;
|
||||||
using MariesWonderland.Extensions;
|
using MariesWonderland.Extensions;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace MariesWonderland;
|
namespace MariesWonderland;
|
||||||
|
|
||||||
@@ -26,6 +29,19 @@ public static class Program
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Load user data on startup
|
||||||
|
UserDataStore userDataStore = app.Services.GetRequiredService<UserDataStore>();
|
||||||
|
ServerOptions serverOptions = app.Services.GetRequiredService<IOptions<ServerOptions>>().Value;
|
||||||
|
string userDatabasePath = Path.IsPathRooted(serverOptions.Data.UserDatabase)
|
||||||
|
? serverOptions.Data.UserDatabase
|
||||||
|
: Path.Combine(AppContext.BaseDirectory, serverOptions.Data.UserDatabase);
|
||||||
|
|
||||||
|
int loadedUsers = userDataStore.Load(userDatabasePath);
|
||||||
|
if (loadedUsers > 0)
|
||||||
|
{
|
||||||
|
app.Logger.LogInformation("Loaded {Count} users from {Path}", loadedUsers, userDatabasePath);
|
||||||
|
}
|
||||||
|
|
||||||
app.MapGrpcServices();
|
app.MapGrpcServices();
|
||||||
app.MapHttpApis();
|
app.MapHttpApis();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MariesWonderland.Proto.GamePlay;
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using MariesWonderland.Proto.Gacha;
|
||||||
|
using MariesWonderland.Proto.GamePlay;
|
||||||
|
|
||||||
namespace MariesWonderland.Services;
|
namespace MariesWonderland.Services;
|
||||||
|
|
||||||
@@ -7,6 +8,16 @@ public class GameplayService : MariesWonderland.Proto.GamePlay.GameplayService.G
|
|||||||
{
|
{
|
||||||
public override Task<CheckBeforeGamePlayResponse> CheckBeforeGamePlay(CheckBeforeGamePlayRequest request, ServerCallContext context)
|
public override Task<CheckBeforeGamePlayResponse> CheckBeforeGamePlay(CheckBeforeGamePlayRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new CheckBeforeGamePlayResponse());
|
CheckBeforeGamePlayResponse response = new()
|
||||||
|
{
|
||||||
|
IsExistUnreadPop = true
|
||||||
|
};
|
||||||
|
response.MenuGachaBadgeInfo.Add(new MenuGachaBadgeInfo()
|
||||||
|
{
|
||||||
|
DisplayStartDatetime = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeOffset.MinValue),
|
||||||
|
DisplayEndDatetime = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeOffset.MaxValue),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,144 @@
|
|||||||
using MariesWonderland.Proto.Gimmick;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using MariesWonderland.Data;
|
||||||
|
using MariesWonderland.Extensions;
|
||||||
|
using MariesWonderland.Models.Entities;
|
||||||
|
using MariesWonderland.Proto.Gimmick;
|
||||||
|
|
||||||
namespace MariesWonderland.Services;
|
namespace MariesWonderland.Services;
|
||||||
|
|
||||||
public class GimmickService : MariesWonderland.Proto.Gimmick.GimmickService.GimmickServiceBase
|
public class GimmickService(UserDataStore store) : MariesWonderland.Proto.Gimmick.GimmickService.GimmickServiceBase
|
||||||
{
|
{
|
||||||
|
private readonly UserDataStore _store = store;
|
||||||
|
|
||||||
public override Task<UpdateSequenceResponse> UpdateSequence(UpdateSequenceRequest request, ServerCallContext context)
|
public override Task<UpdateSequenceResponse> UpdateSequence(UpdateSequenceRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new UpdateSequenceResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
// Ensure the sequence row exists so the client can track it
|
||||||
|
if (!userDb.EntityIUserGimmickSequence.Any(s =>
|
||||||
|
s.UserId == userId &&
|
||||||
|
s.GimmickSequenceScheduleId == request.GimmickSequenceScheduleId &&
|
||||||
|
s.GimmickSequenceId == request.GimmickSequenceId))
|
||||||
|
{
|
||||||
|
userDb.EntityIUserGimmickSequence.Add(new EntityIUserGimmickSequence
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
GimmickSequenceScheduleId = request.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId = request.GimmickSequenceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSequenceResponse response = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<UpdateGimmickProgressResponse> UpdateGimmickProgress(UpdateGimmickProgressRequest request, ServerCallContext context)
|
public override Task<UpdateGimmickProgressResponse> UpdateGimmickProgress(UpdateGimmickProgressRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new UpdateGimmickProgressResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
// Upsert the base gimmick progress row
|
||||||
|
EntityIUserGimmick? gimmick = userDb.EntityIUserGimmick.FirstOrDefault(g =>
|
||||||
|
g.UserId == userId &&
|
||||||
|
g.GimmickSequenceScheduleId == request.GimmickSequenceScheduleId &&
|
||||||
|
g.GimmickSequenceId == request.GimmickSequenceId &&
|
||||||
|
g.GimmickId == request.GimmickId);
|
||||||
|
|
||||||
|
if (gimmick == null)
|
||||||
|
{
|
||||||
|
gimmick = new EntityIUserGimmick
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
GimmickSequenceScheduleId = request.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId = request.GimmickSequenceId,
|
||||||
|
GimmickId = request.GimmickId,
|
||||||
|
};
|
||||||
|
userDb.EntityIUserGimmick.Add(gimmick);
|
||||||
|
}
|
||||||
|
gimmick.StartDatetime = nowMs;
|
||||||
|
|
||||||
|
// Upsert the ornament progress row, recording the progress bit from the client
|
||||||
|
EntityIUserGimmickOrnamentProgress? ornament = userDb.EntityIUserGimmickOrnamentProgress.FirstOrDefault(o =>
|
||||||
|
o.UserId == userId &&
|
||||||
|
o.GimmickSequenceScheduleId == request.GimmickSequenceScheduleId &&
|
||||||
|
o.GimmickSequenceId == request.GimmickSequenceId &&
|
||||||
|
o.GimmickId == request.GimmickId &&
|
||||||
|
o.GimmickOrnamentIndex == request.GimmickOrnamentIndex);
|
||||||
|
|
||||||
|
if (ornament == null)
|
||||||
|
{
|
||||||
|
ornament = new EntityIUserGimmickOrnamentProgress
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
GimmickSequenceScheduleId = request.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId = request.GimmickSequenceId,
|
||||||
|
GimmickId = request.GimmickId,
|
||||||
|
GimmickOrnamentIndex = request.GimmickOrnamentIndex,
|
||||||
|
};
|
||||||
|
userDb.EntityIUserGimmickOrnamentProgress.Add(ornament);
|
||||||
|
}
|
||||||
|
ornament.ProgressValueBit = request.ProgressValueBit;
|
||||||
|
ornament.BaseDatetime = nowMs;
|
||||||
|
|
||||||
|
UpdateGimmickProgressResponse response = new()
|
||||||
|
{
|
||||||
|
IsSequenceCleared = false,
|
||||||
|
};
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<InitSequenceScheduleResponse> InitSequenceSchedule(Empty request, ServerCallContext context)
|
public override Task<InitSequenceScheduleResponse> InitSequenceSchedule(Empty request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new InitSequenceScheduleResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
// Read-only — returns all gimmick tables so the client can initialise its state
|
||||||
|
InitSequenceScheduleResponse response = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<UnlockResponse> Unlock(UnlockRequest request, ServerCallContext context)
|
public override Task<UnlockResponse> Unlock(UnlockRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new UnlockResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
foreach (GimmickKey key in request.GimmickKey)
|
||||||
|
{
|
||||||
|
EntityIUserGimmickUnlock? unlock = userDb.EntityIUserGimmickUnlock.FirstOrDefault(u =>
|
||||||
|
u.UserId == userId &&
|
||||||
|
u.GimmickSequenceScheduleId == key.GimmickSequenceScheduleId &&
|
||||||
|
u.GimmickSequenceId == key.GimmickSequenceId &&
|
||||||
|
u.GimmickId == key.GimmickId);
|
||||||
|
if (unlock == null)
|
||||||
|
{
|
||||||
|
unlock = new EntityIUserGimmickUnlock
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
GimmickSequenceScheduleId = key.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId = key.GimmickSequenceId,
|
||||||
|
GimmickId = key.GimmickId,
|
||||||
|
};
|
||||||
|
userDb.EntityIUserGimmickUnlock.Add(unlock);
|
||||||
|
}
|
||||||
|
unlock.IsUnlocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlockResponse response = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using MariesWonderland.Proto.IndividualPop;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using MariesWonderland.Proto.IndividualPop;
|
||||||
|
|
||||||
namespace MariesWonderland.Services;
|
namespace MariesWonderland.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using MariesWonderland.Proto.Notification;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using MariesWonderland.Proto.Notification;
|
||||||
|
|
||||||
namespace MariesWonderland.Services;
|
namespace MariesWonderland.Services;
|
||||||
|
|
||||||
@@ -8,6 +8,11 @@ public class NotificationService : MariesWonderland.Proto.Notification.Notificat
|
|||||||
{
|
{
|
||||||
public override Task<GetHeaderNotificationResponse> GetHeaderNotification(Empty request, ServerCallContext context)
|
public override Task<GetHeaderNotificationResponse> GetHeaderNotification(Empty request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new GetHeaderNotificationResponse());
|
return Task.FromResult(new GetHeaderNotificationResponse()
|
||||||
|
{
|
||||||
|
FriendRequestReceiveCount = 0,
|
||||||
|
GiftNotReceiveCount = 0,
|
||||||
|
IsExistUnreadInformation = false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,66 @@
|
|||||||
using MariesWonderland.Proto.Quest;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using MariesWonderland.Data;
|
||||||
|
using MariesWonderland.Extensions;
|
||||||
|
using MariesWonderland.Models.Entities;
|
||||||
|
using MariesWonderland.Models.Type;
|
||||||
|
using MariesWonderland.Proto.Quest;
|
||||||
|
|
||||||
namespace MariesWonderland.Services;
|
namespace MariesWonderland.Services;
|
||||||
|
|
||||||
public class QuestService : MariesWonderland.Proto.Quest.QuestService.QuestServiceBase
|
public class QuestService(UserDataStore store, DarkMasterMemoryDatabase masterDb, ILogger<QuestService> logger) : MariesWonderland.Proto.Quest.QuestService.QuestServiceBase
|
||||||
{
|
{
|
||||||
|
private readonly UserDataStore _store = store;
|
||||||
|
private readonly DarkMasterMemoryDatabase _masterDb = masterDb;
|
||||||
|
private readonly ILogger<QuestService> _logger = logger;
|
||||||
|
|
||||||
public override Task<UpdateMainFlowSceneProgressResponse> UpdateMainFlowSceneProgress(UpdateMainFlowSceneProgressRequest request, ServerCallContext context)
|
public override Task<UpdateMainFlowSceneProgressResponse> UpdateMainFlowSceneProgress(UpdateMainFlowSceneProgressRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new UpdateMainFlowSceneProgressResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
int questSceneId = request.QuestSceneId;
|
||||||
|
|
||||||
|
// Resolve questId from the scene, then walk the master data chain to find the route:
|
||||||
|
// QuestScene -> MainQuestSequence (by QuestId) -> MainQuestSequenceGroup (by MainQuestSequenceId)
|
||||||
|
// -> MainQuestChapter (by MainQuestSequenceGroupId) -> MainQuestRouteId
|
||||||
|
EntityMQuestScene? scene = _masterDb.EntityMQuestScene.FirstOrDefault(s => s.QuestSceneId == questSceneId);
|
||||||
|
int routeId = 0;
|
||||||
|
if (scene != null)
|
||||||
|
{
|
||||||
|
EntityMMainQuestSequence? sequence = _masterDb.EntityMMainQuestSequence.FirstOrDefault(s => s.QuestId == scene.QuestId);
|
||||||
|
if (sequence != null)
|
||||||
|
{
|
||||||
|
EntityMMainQuestSequenceGroup? sequenceGroup = _masterDb.EntityMMainQuestSequenceGroup.FirstOrDefault(g => g.MainQuestSequenceId == sequence.MainQuestSequenceId);
|
||||||
|
if (sequenceGroup != null)
|
||||||
|
{
|
||||||
|
EntityMMainQuestChapter? chapter = _masterDb.EntityMMainQuestChapter.FirstOrDefault(c => c.MainQuestSequenceGroupId == sequenceGroup.MainQuestSequenceGroupId);
|
||||||
|
routeId = chapter?.MainQuestRouteId ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityIUserMainQuestFlowStatus flowStatus = userDb.EntityIUserMainQuestFlowStatus.FirstOrDefault(s => s.UserId == userId)
|
||||||
|
?? AddEntity(userDb.EntityIUserMainQuestFlowStatus, new EntityIUserMainQuestFlowStatus { UserId = userId });
|
||||||
|
flowStatus.CurrentQuestFlowType = QuestFlowType.MAIN_FLOW;
|
||||||
|
|
||||||
|
EntityIUserMainQuestMainFlowStatus mainFlowStatus = userDb.EntityIUserMainQuestMainFlowStatus.FirstOrDefault(s => s.UserId == userId)
|
||||||
|
?? AddEntity(userDb.EntityIUserMainQuestMainFlowStatus, new EntityIUserMainQuestMainFlowStatus { UserId = userId });
|
||||||
|
mainFlowStatus.CurrentQuestSceneId = questSceneId;
|
||||||
|
mainFlowStatus.HeadQuestSceneId = Math.Max(mainFlowStatus.HeadQuestSceneId, questSceneId);
|
||||||
|
if (routeId != 0) mainFlowStatus.CurrentMainQuestRouteId = routeId;
|
||||||
|
|
||||||
|
UpdateMainFlowSceneProgressResponse response = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds an entity to a list and returns it (convenience for inline new-entity seeding).</summary>
|
||||||
|
private static T AddEntity<T>(List<T> list, T entity)
|
||||||
|
{
|
||||||
|
list.Add(entity);
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<UpdateReplayFlowSceneProgressResponse> UpdateReplayFlowSceneProgress(UpdateReplayFlowSceneProgressRequest request, ServerCallContext context)
|
public override Task<UpdateReplayFlowSceneProgressResponse> UpdateReplayFlowSceneProgress(UpdateReplayFlowSceneProgressRequest request, ServerCallContext context)
|
||||||
@@ -18,7 +70,57 @@ public class QuestService : MariesWonderland.Proto.Quest.QuestService.QuestServi
|
|||||||
|
|
||||||
public override Task<UpdateMainQuestSceneProgressResponse> UpdateMainQuestSceneProgress(UpdateMainQuestSceneProgressRequest request, ServerCallContext context)
|
public override Task<UpdateMainQuestSceneProgressResponse> UpdateMainQuestSceneProgress(UpdateMainQuestSceneProgressRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new UpdateMainQuestSceneProgressResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
int questSceneId = request.QuestSceneId;
|
||||||
|
|
||||||
|
EntityMQuestScene? scene = _masterDb.EntityMQuestScene.FirstOrDefault(s => s.QuestSceneId == questSceneId);
|
||||||
|
EntityMQuest? quest = scene != null ? _masterDb.EntityMQuest.FirstOrDefault(q => q.QuestId == scene.QuestId) : null;
|
||||||
|
|
||||||
|
// IUserMainQuestMainFlowStatus — tracks current scene position and whether we've reached the last scene
|
||||||
|
EntityIUserMainQuestMainFlowStatus mainFlowStatus = userDb.EntityIUserMainQuestMainFlowStatus.FirstOrDefault(s => s.UserId == userId)
|
||||||
|
?? AddEntity(userDb.EntityIUserMainQuestMainFlowStatus, new EntityIUserMainQuestMainFlowStatus { UserId = userId });
|
||||||
|
mainFlowStatus.CurrentQuestSceneId = questSceneId;
|
||||||
|
mainFlowStatus.HeadQuestSceneId = Math.Max(mainFlowStatus.HeadQuestSceneId, questSceneId);
|
||||||
|
if (scene?.QuestResultType is QuestResultType.HALF_RESULT or QuestResultType.FULL_RESULT)
|
||||||
|
mainFlowStatus.IsReachedLastQuestScene = true;
|
||||||
|
|
||||||
|
// IUserMainQuestFlowStatus and IUserMainQuestProgressStatus — sub-flow tracking for playable quests
|
||||||
|
bool isPlayable = quest != null && !quest.IsRunInTheBackground && quest.IsCountedAsQuest;
|
||||||
|
if (isPlayable)
|
||||||
|
{
|
||||||
|
EntityIUserMainQuestFlowStatus flowStatus = userDb.EntityIUserMainQuestFlowStatus.FirstOrDefault(s => s.UserId == userId)
|
||||||
|
?? AddEntity(userDb.EntityIUserMainQuestFlowStatus, new EntityIUserMainQuestFlowStatus { UserId = userId });
|
||||||
|
flowStatus.CurrentQuestFlowType = QuestFlowType.SUB_FLOW;
|
||||||
|
|
||||||
|
EntityIUserMainQuestProgressStatus progressStatus = userDb.EntityIUserMainQuestProgressStatus.FirstOrDefault(s => s.UserId == userId)
|
||||||
|
?? AddEntity(userDb.EntityIUserMainQuestProgressStatus, new EntityIUserMainQuestProgressStatus { UserId = userId });
|
||||||
|
progressStatus.CurrentQuestSceneId = questSceneId;
|
||||||
|
progressStatus.HeadQuestSceneId = Math.Max(progressStatus.HeadQuestSceneId, questSceneId);
|
||||||
|
progressStatus.CurrentQuestFlowType = QuestFlowType.SUB_FLOW;
|
||||||
|
|
||||||
|
// Reaching a result scene clears the quest; a full-result scene also increments clear counts
|
||||||
|
if (scene?.QuestResultType is QuestResultType.HALF_RESULT or QuestResultType.FULL_RESULT)
|
||||||
|
{
|
||||||
|
EntityIUserQuest userQuest = userDb.EntityIUserQuest.FirstOrDefault(q => q.UserId == userId && q.QuestId == quest!.QuestId)
|
||||||
|
?? AddEntity(userDb.EntityIUserQuest, new EntityIUserQuest { UserId = userId, QuestId = quest!.QuestId });
|
||||||
|
userQuest.QuestStateType = (int)QuestStateType.CLEARED;
|
||||||
|
|
||||||
|
if (scene.QuestResultType == QuestResultType.FULL_RESULT)
|
||||||
|
{
|
||||||
|
userQuest.ClearCount++;
|
||||||
|
userQuest.DailyClearCount++;
|
||||||
|
userQuest.LastClearDatetime = nowMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateMainQuestSceneProgressResponse response = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<UpdateExtraQuestSceneProgressResponse> UpdateExtraQuestSceneProgress(UpdateExtraQuestSceneProgressRequest request, ServerCallContext context)
|
public override Task<UpdateExtraQuestSceneProgressResponse> UpdateExtraQuestSceneProgress(UpdateExtraQuestSceneProgressRequest request, ServerCallContext context)
|
||||||
@@ -33,7 +135,62 @@ public class QuestService : MariesWonderland.Proto.Quest.QuestService.QuestServi
|
|||||||
|
|
||||||
public override Task<StartMainQuestResponse> StartMainQuest(StartMainQuestRequest request, ServerCallContext context)
|
public override Task<StartMainQuestResponse> StartMainQuest(StartMainQuestRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new StartMainQuestResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
int questId = request.QuestId;
|
||||||
|
|
||||||
|
EntityMQuest? quest = _masterDb.EntityMQuest.FirstOrDefault(q => q.QuestId == questId);
|
||||||
|
|
||||||
|
// Ensure user quest and mission rows exist before any state transitions.
|
||||||
|
EntityIUserQuest userQuest = userDb.EntityIUserQuest.FirstOrDefault(q => q.UserId == userId && q.QuestId == questId)
|
||||||
|
?? AddEntity(userDb.EntityIUserQuest, new EntityIUserQuest { UserId = userId, QuestId = questId });
|
||||||
|
|
||||||
|
if (quest != null && quest.QuestMissionGroupId != 0)
|
||||||
|
{
|
||||||
|
List<int> missionIds = [.. _masterDb.EntityMQuestMissionGroup
|
||||||
|
.Where(g => g.QuestMissionGroupId == quest.QuestMissionGroupId)
|
||||||
|
.Select(g => g.QuestMissionId)];
|
||||||
|
|
||||||
|
foreach (int missionId in missionIds)
|
||||||
|
{
|
||||||
|
if (!userDb.EntityIUserQuestMission.Any(m => m.UserId == userId && m.QuestId == questId && m.QuestMissionId == missionId))
|
||||||
|
userDb.EntityIUserQuestMission.Add(new EntityIUserQuestMission { UserId = userId, QuestId = questId, QuestMissionId = missionId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't touch state on already-cleared quests (e.g. replaying a finished story quest).
|
||||||
|
if (userQuest.QuestStateType == (int)QuestStateType.CLEARED)
|
||||||
|
{
|
||||||
|
StartMainQuestResponse earlyResponse = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) earlyResponse.DiffUserData[k] = v;
|
||||||
|
return Task.FromResult(earlyResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
userQuest.IsBattleOnly = request.IsBattleOnly;
|
||||||
|
|
||||||
|
// Background quests (cutscenes, non-combat) have no playable content — auto-clear them immediately.
|
||||||
|
// Playable quests transition to ACTIVE so the client knows a quest session is in progress.
|
||||||
|
bool isPlayable = quest != null && !quest.IsRunInTheBackground && quest.IsCountedAsQuest;
|
||||||
|
if (isPlayable)
|
||||||
|
{
|
||||||
|
userQuest.QuestStateType = (int)QuestStateType.ACTIVE;
|
||||||
|
userQuest.LatestStartDatetime = nowMs;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userQuest.QuestStateType = (int)QuestStateType.CLEARED;
|
||||||
|
userQuest.ClearCount++;
|
||||||
|
userQuest.DailyClearCount++;
|
||||||
|
userQuest.LastClearDatetime = nowMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartMainQuestResponse response = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<RestartMainQuestResponse> RestartMainQuest(RestartMainQuestRequest request, ServerCallContext context)
|
public override Task<RestartMainQuestResponse> RestartMainQuest(RestartMainQuestRequest request, ServerCallContext context)
|
||||||
|
|||||||
@@ -1,17 +1,105 @@
|
|||||||
using MariesWonderland.Proto.Tutorial;
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using MariesWonderland.Data;
|
||||||
|
using MariesWonderland.Extensions;
|
||||||
|
using MariesWonderland.Models.Entities;
|
||||||
|
using MariesWonderland.Models.Type;
|
||||||
|
using MariesWonderland.Proto.Tutorial;
|
||||||
|
|
||||||
namespace MariesWonderland.Services;
|
namespace MariesWonderland.Services;
|
||||||
|
|
||||||
public class TutorialService : MariesWonderland.Proto.Tutorial.TutorialService.TutorialServiceBase
|
public class TutorialService(UserDataStore store) : MariesWonderland.Proto.Tutorial.TutorialService.TutorialServiceBase
|
||||||
{
|
{
|
||||||
public override Task<SetTutorialProgressResponse> SetTutorialProgress(SetTutorialProgressRequest request, ServerCallContext context)
|
public override Task<SetTutorialProgressResponse> SetTutorialProgress(SetTutorialProgressRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new SetTutorialProgressResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
UpsertTutorialProgress(userDb, userId, (TutorialType)request.TutorialType, request.ProgressPhase, request.ChoiceId);
|
||||||
|
|
||||||
|
SetTutorialProgressResponse response = new();
|
||||||
|
foreach ((string k, Proto.Data.DiffData v) in UserDataDiffBuilder.Delta(before, userDb))
|
||||||
|
response.DiffUserData[k] = v;
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<SetTutorialProgressAndReplaceDeckResponse> SetTutorialProgressAndReplaceDeck(SetTutorialProgressAndReplaceDeckRequest request, ServerCallContext context)
|
public override Task<SetTutorialProgressAndReplaceDeckResponse> SetTutorialProgressAndReplaceDeck(SetTutorialProgressAndReplaceDeckRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new SetTutorialProgressAndReplaceDeckResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = store.GetOrCreate(userId);
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
UpsertTutorialProgress(userDb, userId, (TutorialType)request.TutorialType, request.ProgressPhase, choiceId: 0);
|
||||||
|
UpsertDeck(userDb, userId, request);
|
||||||
|
|
||||||
|
SetTutorialProgressAndReplaceDeckResponse response = new();
|
||||||
|
foreach ((string k, Proto.Data.DiffData v) in UserDataDiffBuilder.Delta(before, userDb))
|
||||||
|
response.DiffUserData[k] = v;
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpsertTutorialProgress(DarkUserMemoryDatabase db, long userId, TutorialType type, int phase, int choiceId)
|
||||||
|
{
|
||||||
|
EntityIUserTutorialProgress? existing = db.EntityIUserTutorialProgress
|
||||||
|
.FirstOrDefault(t => t.UserId == userId && t.TutorialType == type);
|
||||||
|
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
db.EntityIUserTutorialProgress.Add(new EntityIUserTutorialProgress
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
TutorialType = type,
|
||||||
|
ProgressPhase = phase,
|
||||||
|
ChoiceId = choiceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.ProgressPhase = phase;
|
||||||
|
existing.ChoiceId = choiceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upserts the user's deck from a SetTutorialProgressAndReplaceDeck request.
|
||||||
|
/// During tutorial the client sends the deck it wants to play with — we record the
|
||||||
|
/// costume UUIDs from each slot and seed sensible defaults for name and power.
|
||||||
|
/// </summary>
|
||||||
|
private static void UpsertDeck(DarkUserMemoryDatabase db, long userId, SetTutorialProgressAndReplaceDeckRequest request)
|
||||||
|
{
|
||||||
|
DeckType deckType = (DeckType)request.DeckType;
|
||||||
|
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
string uuid01 = request.Deck?.Character01?.UserCostumeUuid ?? "";
|
||||||
|
string uuid02 = request.Deck?.Character02?.UserCostumeUuid ?? "";
|
||||||
|
string uuid03 = request.Deck?.Character03?.UserCostumeUuid ?? "";
|
||||||
|
|
||||||
|
EntityIUserDeck? existing = db.EntityIUserDeck
|
||||||
|
.FirstOrDefault(d => d.UserId == userId && d.DeckType == deckType && d.UserDeckNumber == request.UserDeckNumber);
|
||||||
|
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
db.EntityIUserDeck.Add(new EntityIUserDeck(
|
||||||
|
UserId: userId,
|
||||||
|
DeckType: deckType,
|
||||||
|
UserDeckNumber: request.UserDeckNumber,
|
||||||
|
UserDeckCharacterUuid01: uuid01,
|
||||||
|
UserDeckCharacterUuid02: uuid02,
|
||||||
|
UserDeckCharacterUuid03: uuid03,
|
||||||
|
Name: "Deck 1",
|
||||||
|
Power: 100,
|
||||||
|
LatestVersion: now
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(uuid01)) existing.UserDeckCharacterUuid01 = uuid01;
|
||||||
|
if (!string.IsNullOrEmpty(uuid02)) existing.UserDeckCharacterUuid02 = uuid02;
|
||||||
|
if (!string.IsNullOrEmpty(uuid03)) existing.UserDeckCharacterUuid03 = uuid03;
|
||||||
|
existing.LatestVersion = now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,18 @@ public class UserService(UserDataStore store) : MariesWonderland.Proto.User.User
|
|||||||
|
|
||||||
public override Task<GameStartResponse> GameStart(Empty request, ServerCallContext context)
|
public override Task<GameStartResponse> GameStart(Empty request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new GameStartResponse());
|
long userId = context.GetUserId();
|
||||||
|
DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId);
|
||||||
|
|
||||||
|
Dictionary<string, string> before = UserDataDiffBuilder.Snapshot(userDb);
|
||||||
|
|
||||||
|
EntityIUser user = userDb.EntityIUser.FirstOrDefault(u => u.UserId == userId)
|
||||||
|
?? AddEntity(userDb.EntityIUser, new EntityIUser { UserId = userId });
|
||||||
|
user.GameStartDatetime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
GameStartResponse response = new();
|
||||||
|
foreach (var (k, v) in UserDataDiffBuilder.Delta(before, userDb)) response.DiffUserData[k] = v;
|
||||||
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<GetBackupTokenResponse> GetBackupToken(GetBackupTokenRequest request, ServerCallContext context)
|
public override Task<GetBackupTokenResponse> GetBackupToken(GetBackupTokenRequest request, ServerCallContext context)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"Data": {
|
"Data": {
|
||||||
"LatestMasterDataVersion": "20240404193219",
|
"LatestMasterDataVersion": "20240404193219",
|
||||||
"UserDataPath": "Data/UserData",
|
"UserDataPath": "Data/UserData",
|
||||||
"MasterDataPath": "Data/MasterData"
|
"MasterDataPath": "Data/MasterData",
|
||||||
|
"UserDatabase": "userdata.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"Data": {
|
"Data": {
|
||||||
"LatestMasterDataVersion": "",
|
"LatestMasterDataVersion": "",
|
||||||
"UserDataPath": "",
|
"UserDataPath": "",
|
||||||
"MasterDataPath": ""
|
"MasterDataPath": "",
|
||||||
|
"UserDatabase": "userdata.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user