feat: Implement missions 1-5 (combat fluidity, urban crowd locomotion, smart traffic signals, dynamic gacha pity, universal item spawner & armory)
This commit is contained in:
1 parent
5e333d7678
commit
ce1f471eab
27 files changed
+321970
-135
No files matched your search
@@ -0,0 +1,194 @@
|
||||
using System.Text.Json;
|
||||
using Ananta.SDK.Logging;
|
||||
using Ananta.Server.Configuration;
|
||||
|
||||
namespace Ananta.Server.Protocol.Client4229938;
|
||||
|
||||
/// <summary>Wallet + backpack + gacha counters kept in memory for the lifetime of a login.</summary>
|
||||
internal sealed class PlayerProgress
|
||||
{
|
||||
internal double Money { get; set; }
|
||||
internal double Gold { get; set; }
|
||||
internal double BindingGold { get; set; }
|
||||
internal List<PlayerProgressItem> Items { get; } = [];
|
||||
internal Dictionary<uint, uint> GachaDrawsSinceGrand { get; } = [];
|
||||
internal HashSet<uint> Fashions { get; } = [];
|
||||
internal HashSet<uint> Vehicles { get; } = [];
|
||||
internal bool RestoredFromDisk { get; set; }
|
||||
}
|
||||
|
||||
internal sealed record PlayerProgressItem(uint TemplateId, uint Count, bool IsBind, ulong UniqueId);
|
||||
|
||||
/// <summary>
|
||||
/// Minimal character economy persistence for build 4229938. The retail server keeps this in the
|
||||
/// account database; this private server writes one JSON snapshot per player next to
|
||||
/// <c>config/private-server.json</c> and replays it through PlayerInfo at login.
|
||||
/// </summary>
|
||||
internal static class PlayerProgressStore
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = true };
|
||||
|
||||
internal static string FilePath { get; } =
|
||||
PrivateServerConfigStore.ResolveProjectPath(Path.Combine("config", "player-progress.json"));
|
||||
|
||||
internal static void LoadInto(PlayerProgress progress, ServerLogger log)
|
||||
{
|
||||
var path = FilePath;
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
log.Info($"[ECONOMY] no saved character economy yet ({path})");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SaveFile? file;
|
||||
lock (Gate)
|
||||
file = JsonSerializer.Deserialize<SaveFile>(File.ReadAllText(path));
|
||||
|
||||
if (file is null)
|
||||
{
|
||||
log.Warn($"[ECONOMY] saved character economy is empty: {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
progress.Money = file.Money;
|
||||
progress.Gold = file.Gold;
|
||||
progress.BindingGold = file.BindingGold;
|
||||
progress.Items.Clear();
|
||||
progress.GachaDrawsSinceGrand.Clear();
|
||||
progress.Fashions.Clear();
|
||||
progress.Vehicles.Clear();
|
||||
|
||||
foreach (var item in file.Items)
|
||||
{
|
||||
if (item.TemplateId == 0 || item.Count == 0)
|
||||
continue;
|
||||
|
||||
progress.Items.RemoveAll(x => x.TemplateId == item.TemplateId);
|
||||
progress.Items.Add(new PlayerProgressItem(item.TemplateId, item.Count, item.IsBind, item.UniqueId));
|
||||
}
|
||||
|
||||
foreach (var (poolId, draws) in file.GachaDraws)
|
||||
{
|
||||
if (poolId != 0)
|
||||
progress.GachaDrawsSinceGrand[poolId] = draws;
|
||||
}
|
||||
|
||||
foreach (var fashionId in file.Fashions)
|
||||
{
|
||||
if (fashionId != 0)
|
||||
progress.Fashions.Add(fashionId);
|
||||
}
|
||||
|
||||
foreach (var vehicleId in file.Vehicles)
|
||||
{
|
||||
if (vehicleId != 0)
|
||||
progress.Vehicles.Add(vehicleId);
|
||||
}
|
||||
|
||||
progress.RestoredFromDisk = true;
|
||||
log.Info(
|
||||
$"[ECONOMY] restored pid={file.Pid} account={file.AccountId} " +
|
||||
$"money={progress.Money} gold={progress.Gold} bindingGold={progress.BindingGold} " +
|
||||
$"items={progress.Items.Count} gachaPools={progress.GachaDrawsSinceGrand.Count} " +
|
||||
$"fashions={progress.Fashions.Count} vehicles={progress.Vehicles.Count} ({path})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Warn($"[ECONOMY] saved character economy could not be read ({ex.GetType().Name}: {ex.Message}): {path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Snapshots the live session state (wallet, GM/granted backpack, gacha counters) to disk.</summary>
|
||||
internal static void Save(WorldEntryState state, ServerLogger log)
|
||||
{
|
||||
var file = new SaveFile
|
||||
{
|
||||
Pid = PrivateServerConfigStore.Current.Player.Pid,
|
||||
AccountId = PrivateServerConfigStore.Current.Player.AccountId,
|
||||
Money = state.WalletMoney,
|
||||
Gold = state.WalletGold,
|
||||
BindingGold = state.WalletBindingGold,
|
||||
SavedAtUtc = DateTime.UtcNow.ToString("O"),
|
||||
};
|
||||
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
foreach (var stack in state.GmBackpackItems.Values)
|
||||
{
|
||||
if (stack.TemplateId == 0 || stack.Count == 0)
|
||||
continue;
|
||||
|
||||
file.Items.Add(new SaveItem
|
||||
{
|
||||
TemplateId = stack.TemplateId,
|
||||
Count = stack.Count,
|
||||
IsBind = stack.IsBind,
|
||||
UniqueId = stack.UniqueId,
|
||||
});
|
||||
}
|
||||
|
||||
file.Fashions.AddRange(state.Progress.Fashions.Where(x => x != 0).OrderBy(x => x));
|
||||
file.Vehicles.AddRange(state.Progress.Vehicles.Where(x => x != 0).OrderBy(x => x));
|
||||
}
|
||||
|
||||
foreach (var (poolId, draws) in state.Progress.GachaDrawsSinceGrand)
|
||||
file.GachaDraws[poolId] = draws;
|
||||
|
||||
var path = FilePath;
|
||||
var temp = path + ".tmp";
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.WriteAllText(temp, JsonSerializer.Serialize(file, Options));
|
||||
File.Move(temp, path, overwrite: true);
|
||||
}
|
||||
|
||||
log.Info(
|
||||
$"[ECONOMY] saved character economy money={file.Money} gold={file.Gold} " +
|
||||
$"bindingGold={file.BindingGold} items={file.Items.Count} " +
|
||||
$"fashions={file.Fashions.Count} vehicles={file.Vehicles.Count} -> {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Warn($"[ECONOMY] character economy could not be saved ({ex.GetType().Name}: {ex.Message}): {path}");
|
||||
try
|
||||
{
|
||||
if (File.Exists(temp))
|
||||
File.Delete(temp);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SaveFile
|
||||
{
|
||||
public ulong Pid { get; set; }
|
||||
public string AccountId { get; set; } = string.Empty;
|
||||
public double Money { get; set; }
|
||||
public double Gold { get; set; }
|
||||
public double BindingGold { get; set; }
|
||||
public string SavedAtUtc { get; set; } = string.Empty;
|
||||
public List<SaveItem> Items { get; set; } = [];
|
||||
public Dictionary<uint, uint> GachaDraws { get; set; } = [];
|
||||
public List<uint> Fashions { get; set; } = [];
|
||||
public List<uint> Vehicles { get; set; } = [];
|
||||
}
|
||||
|
||||
private sealed class SaveItem
|
||||
{
|
||||
public uint TemplateId { get; set; }
|
||||
public uint Count { get; set; }
|
||||
public bool IsBind { get; set; }
|
||||
public ulong UniqueId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -488,4 +488,20 @@ internal static class RuntimePayloadFactory
|
||||
|
||||
private static Auto.UXVector3 Vector(Vec3 value)
|
||||
=> new() { X = value.X, Y = value.Y, Z = value.Z };
|
||||
|
||||
internal static Auto.PlayerPackItem GmPackItem4229938(uint templateId, uint count, bool isBind, ulong uniqueId)
|
||||
=> new()
|
||||
{
|
||||
UniqueId = uniqueId,
|
||||
TemplateId = templateId,
|
||||
Count = count,
|
||||
IsNew = false,
|
||||
ExpiryTime = 0,
|
||||
RemindState = 0,
|
||||
Quality = 0,
|
||||
Tags = 0,
|
||||
IsBind = isBind,
|
||||
Components = null,
|
||||
CDFinishTime = 0,
|
||||
};
|
||||
}
|
||||
@@ -53,19 +53,29 @@ internal static class WorldCodec
|
||||
internal static SceneMethods.SyncSceneLoadCompleted SceneLoadCompleted(ulong sceneId)
|
||||
=> new() { sceneId = sceneId };
|
||||
|
||||
internal static SceneMethods.SyncUnitPositionAndFacing PositionAndFacing(ulong unitId, Vec3 position, float facing, byte setPositionType = 3)
|
||||
internal static SceneMethods.SyncUnitPositionAndFacing PositionAndFacing(
|
||||
ulong unitId, Vec3 position, float facing, byte setPositionType, bool continueMove, byte moveId)
|
||||
=> new()
|
||||
{
|
||||
unitId = unitId,
|
||||
position = new SceneMethods.UxVector3(position.X, position.Y, position.Z),
|
||||
facing = facing,
|
||||
moveId = 0,
|
||||
continueMove = false,
|
||||
moveId = moveId,
|
||||
continueMove = continueMove,
|
||||
setPositionType = setPositionType,
|
||||
moveGroundInfo = null,
|
||||
loadingInfo = null,
|
||||
};
|
||||
|
||||
internal static SceneMethods.SyncUnitPositionAndFacing PositionAndFacing(
|
||||
ulong unitId, Vec3 position, float facing, byte setPositionType = 3)
|
||||
=> PositionAndFacing(unitId, position, facing, setPositionType, continueMove: false, moveId: MoveAction.Unknown);
|
||||
|
||||
internal static SceneMethods.SyncUnitPositionAndFacing Walking(
|
||||
ulong unitId, Vec3 position, float facing, byte moveId = MoveAction.WalkFront,
|
||||
byte setPositionType = SetPositionType.Gm, bool continueMove = true)
|
||||
=> PositionAndFacing(unitId, position, facing, setPositionType, continueMove, moveId);
|
||||
|
||||
internal static SceneMethods.SyncUnitPositionAndFacing PositionAndFacing(Vec3 position, float facing, byte setPositionType = 3)
|
||||
=> PositionAndFacing(Profile.InitialUnitId, position, facing, setPositionType);
|
||||
|
||||
@@ -161,4 +171,29 @@ internal static class WorldCodec
|
||||
|
||||
internal static SceneMethods.SyncRemoveUnitState RemoveUnitState(uint state)
|
||||
=> RemoveUnitState(Profile.InitialUnitId, state);
|
||||
|
||||
internal static class MoveAction
|
||||
{
|
||||
internal const byte Unknown = 0;
|
||||
internal const byte Idle = 1;
|
||||
internal const byte WalkFront = 2;
|
||||
internal const byte WalkBack = 3;
|
||||
internal const byte WalkLeft = 4;
|
||||
internal const byte WalkRight = 5;
|
||||
internal const byte Run = 6;
|
||||
}
|
||||
|
||||
internal static class SetPositionType
|
||||
{
|
||||
internal const byte Force = 0;
|
||||
internal const byte RejectSync = 1;
|
||||
internal const byte Revive = 2;
|
||||
internal const byte SwitchSpirit = 3;
|
||||
internal const byte SpoonNoLoading = 4;
|
||||
internal const byte FallGround = 5;
|
||||
internal const byte Teleport = 6;
|
||||
internal const byte Gm = 7;
|
||||
internal const byte Portal = 8;
|
||||
internal const byte OutOfStuck = 9;
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,17 @@ internal sealed class WorldEntryState
|
||||
// (loaded magazine + reserve). Keep it instance-persistent for the same reason as the magazine.
|
||||
internal Dictionary<ulong, int> WeaponDurabilityAmmo { get; } = [];
|
||||
internal Dictionary<ulong, uint> WeaponBulletByInstance { get; } = [];
|
||||
internal Dictionary<int, uint> SkillByInstanceId { get; } = [];
|
||||
internal Dictionary<uint, uint> BackpackItemCounts { get; } = [];
|
||||
internal Dictionary<uint, RpcTypes.Client4229938.Auto.PlayerPackItem> GmBackpackItems { get; } = [];
|
||||
internal double WalletMoney { get; set; }
|
||||
internal double WalletGold { get; set; }
|
||||
internal double WalletBindingGold { get; set; }
|
||||
internal PlayerProgress Progress { get; } = new();
|
||||
internal bool ProgressRestored { get; set; }
|
||||
internal bool HasSessionEconomy4229938()
|
||||
=> WalletMoney != 0 || WalletGold != 0 || WalletBindingGold != 0 || GmBackpackItems.Count > 0;
|
||||
internal HashSet<ulong> ArmoryWeaponsAnnounced { get; } = [];
|
||||
internal Dictionary<(uint SpiritId, uint FightStyleTypeId), uint> SpiritStyleOverrides { get; } = [];
|
||||
internal int CombatUseCount { get; set; }
|
||||
internal int CombatEndCount { get; set; }
|
||||
|
||||
Reference in new issue
Block a user