This commit is contained in:
mochaisvibecoded
2026-08-24 17:00:26 -04:00
parent cc1148050c
commit 42f7f57b72
401 changed files with 1108081 additions and 0 deletions
+619
View File
@@ -0,0 +1,619 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Mocha2023.Classes
{
public static class PlayerInventoryStore
{
private static readonly object InventoryLock = new();
private static readonly JsonSerializerOptions ReadOptions = new()
{
PropertyNameCaseInsensitive = true
};
private static readonly JsonSerializerOptions WriteOptions = new()
{
WriteIndented = true
};
public sealed class AvatarItemOwnership
{
public long AvatarItemId { get; set; }
public string AvatarItemDesc { get; set; } = string.Empty;
public string FriendlyName { get; set; } = string.Empty;
}
public sealed class ConsumableOwnership
{
public long ConsumableItemId { get; set; }
public string ConsumableItemDesc { get; set; } = string.Empty;
public string FriendlyName { get; set; } = string.Empty;
public int Quantity { get; set; }
}
public static string GetAvatarItemsPath(long accountId)
{
string directory = Path.Combine(
Program.dataDir,
"AvatarItemsUnlocked",
accountId.ToString());
Directory.CreateDirectory(directory);
return Path.Combine(directory, "AvatarItems.json");
}
public static string GetConsumablesPath(long accountId)
{
string directory = Path.Combine(
Program.dataDir,
"ConsumablesUnlocked",
accountId.ToString());
Directory.CreateDirectory(directory);
return Path.Combine(directory, "Consumables.json");
}
public static void EnsureInitialized(
long accountId,
IEnumerable<string>? legacyAvatarDescriptors = null)
{
EnsureAvatarItemsInitialized(accountId, legacyAvatarDescriptors);
EnsureConsumablesInitialized(accountId);
}
public static List<AvatarItemOwnership> GetAvatarCatalog() =>
LoadAvatarCatalog();
public static List<ConsumableOwnership> GetConsumableCatalog() =>
LoadConsumableCatalog();
public static void EnsureAvatarItemsInitialized(
long accountId,
IEnumerable<string>? legacyAvatarDescriptors = null)
{
lock (InventoryLock)
{
string playerPath = GetAvatarItemsPath(accountId);
if (File.Exists(playerPath))
return;
List<AvatarItemOwnership> owned = LoadAvatarCatalog()
.Where(item => !IsSpecialAvatarItem(item))
.ToList();
HashSet<string> legacy = (legacyAvatarDescriptors ?? Array.Empty<string>())
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
if (legacy.Count > 0)
{
foreach (AvatarItemOwnership item in LoadAvatarCatalog())
{
if (!legacy.Contains(item.AvatarItemDesc))
continue;
if (owned.Any(value => SameAvatar(value, item)))
continue;
owned.Add(item);
}
foreach (string descriptor in legacy)
{
if (owned.Any(value => string.Equals(
value.AvatarItemDesc,
descriptor,
StringComparison.OrdinalIgnoreCase)))
continue;
owned.Add(new AvatarItemOwnership
{
AvatarItemDesc = descriptor,
FriendlyName = descriptor
});
}
}
SaveAvatarItemsUnsafe(playerPath, owned);
}
}
public static void EnsureConsumablesInitialized(long accountId)
{
lock (InventoryLock)
{
string playerPath = GetConsumablesPath(accountId);
if (File.Exists(playerPath))
return;
SaveConsumablesUnsafe(playerPath, LoadConsumableCatalog());
}
}
public static List<AvatarItemOwnership> GetAvatarItems(
long accountId,
IEnumerable<string>? legacyAvatarDescriptors = null)
{
lock (InventoryLock)
{
EnsureAvatarItemsInitialized(accountId, legacyAvatarDescriptors);
try
{
return JsonSerializer.Deserialize<List<AvatarItemOwnership>>(
File.ReadAllText(GetAvatarItemsPath(accountId)),
ReadOptions)
?? new List<AvatarItemOwnership>();
}
catch (JsonException)
{
return new List<AvatarItemOwnership>();
}
}
}
public static List<ConsumableOwnership> GetConsumables(long accountId)
{
lock (InventoryLock)
{
EnsureConsumablesInitialized(accountId);
try
{
return JsonSerializer.Deserialize<List<ConsumableOwnership>>(
File.ReadAllText(GetConsumablesPath(accountId)),
ReadOptions)
?? new List<ConsumableOwnership>();
}
catch (JsonException)
{
return new List<ConsumableOwnership>();
}
}
}
public static bool OwnsAvatarItem(
long accountId,
string descriptor,
long avatarItemId = 0,
IEnumerable<string>? legacyAvatarDescriptors = null)
{
if (string.IsNullOrWhiteSpace(descriptor) && avatarItemId <= 0)
return false;
return GetAvatarItems(accountId, legacyAvatarDescriptors).Any(item =>
(!string.IsNullOrWhiteSpace(descriptor) && string.Equals(
item.AvatarItemDesc,
descriptor,
StringComparison.OrdinalIgnoreCase)) ||
(avatarItemId > 0 && item.AvatarItemId == avatarItemId));
}
public static bool SetAvatarItemOwned(
long accountId,
string descriptor,
long avatarItemId,
string? friendlyName,
bool owned,
IEnumerable<string>? legacyAvatarDescriptors = null)
{
if (string.IsNullOrWhiteSpace(descriptor) && avatarItemId <= 0)
return false;
lock (InventoryLock)
{
List<AvatarItemOwnership> items = GetAvatarItems(
accountId,
legacyAvatarDescriptors);
int index = items.FindIndex(item =>
(!string.IsNullOrWhiteSpace(descriptor) && string.Equals(
item.AvatarItemDesc,
descriptor,
StringComparison.OrdinalIgnoreCase)) ||
(avatarItemId > 0 && item.AvatarItemId == avatarItemId));
if (owned)
{
if (index < 0)
{
items.Add(new AvatarItemOwnership
{
AvatarItemId = avatarItemId,
AvatarItemDesc = descriptor?.Trim() ?? string.Empty,
FriendlyName = string.IsNullOrWhiteSpace(friendlyName)
? descriptor?.Trim() ?? $"Avatar Item {avatarItemId}"
: friendlyName.Trim()
});
}
else
{
if (avatarItemId > 0)
items[index].AvatarItemId = avatarItemId;
if (!string.IsNullOrWhiteSpace(descriptor))
items[index].AvatarItemDesc = descriptor.Trim();
if (!string.IsNullOrWhiteSpace(friendlyName))
items[index].FriendlyName = friendlyName.Trim();
}
}
else if (index >= 0)
{
items.RemoveAt(index);
}
SaveAvatarItemsUnsafe(GetAvatarItemsPath(accountId), items);
return true;
}
}
public static int GetConsumableQuantity(
long accountId,
string descriptor,
long consumableItemId = 0)
{
if (string.IsNullOrWhiteSpace(descriptor) && consumableItemId <= 0)
return 0;
return SumConsumableQuantity(
GetConsumables(accountId).Where(value =>
SameConsumable(value, descriptor, consumableItemId)));
}
public static int SetConsumableQuantity(
long accountId,
string descriptor,
long consumableItemId,
string? friendlyName,
int quantity)
{
quantity = Math.Clamp(quantity, 0, 100_000);
if (string.IsNullOrWhiteSpace(descriptor) && consumableItemId <= 0)
return 0;
lock (InventoryLock)
{
List<ConsumableOwnership> items = GetConsumables(accountId);
List<int> matchingIndexes = items
.Select((value, index) => new { value, index })
.Where(entry => SameConsumable(
entry.value,
descriptor,
consumableItemId))
.Select(entry => entry.index)
.ToList();
if (quantity <= 0)
{
for (int match = matchingIndexes.Count - 1; match >= 0; match--)
items.RemoveAt(matchingIndexes[match]);
}
else if (matchingIndexes.Count == 0)
{
items.Add(new ConsumableOwnership
{
ConsumableItemId = consumableItemId,
ConsumableItemDesc = descriptor?.Trim() ?? string.Empty,
FriendlyName = string.IsNullOrWhiteSpace(friendlyName)
? descriptor?.Trim() ?? $"Consumable {consumableItemId}"
: friendlyName.Trim(),
Quantity = quantity
});
}
else
{
int index = matchingIndexes[0];
items[index].Quantity = quantity;
if (consumableItemId > 0)
items[index].ConsumableItemId = consumableItemId;
if (!string.IsNullOrWhiteSpace(descriptor))
items[index].ConsumableItemDesc = descriptor.Trim();
if (!string.IsNullOrWhiteSpace(friendlyName))
items[index].FriendlyName = friendlyName.Trim();
for (int match = matchingIndexes.Count - 1; match >= 1; match--)
items.RemoveAt(matchingIndexes[match]);
}
SaveConsumablesUnsafe(GetConsumablesPath(accountId), items);
return quantity;
}
}
public static int AddConsumable(
long accountId,
string descriptor,
long consumableItemId,
string? friendlyName,
int amount)
{
if (amount <= 0)
return GetConsumableQuantity(accountId, descriptor, consumableItemId);
lock (InventoryLock)
{
int current = GetConsumableQuantity(
accountId,
descriptor,
consumableItemId);
int next = (int)Math.Min(100_000L, (long)current + amount);
return SetConsumableQuantity(
accountId,
descriptor,
consumableItemId,
friendlyName,
next);
}
}
public static bool TryConsumeConsumable(
long accountId,
string descriptor,
long consumableItemId,
int amount,
out int previousQuantity,
out int remainingQuantity)
{
previousQuantity = 0;
remainingQuantity = 0;
if (amount <= 0 ||
(string.IsNullOrWhiteSpace(descriptor) && consumableItemId <= 0))
{
return false;
}
lock (InventoryLock)
{
List<ConsumableOwnership> items = GetConsumables(accountId);
List<int> matchingIndexes = items
.Select((value, index) => new { value, index })
.Where(entry => SameConsumable(
entry.value,
descriptor,
consumableItemId))
.Select(entry => entry.index)
.ToList();
previousQuantity = SumConsumableQuantity(
matchingIndexes.Select(index => items[index]));
if (matchingIndexes.Count == 0 || previousQuantity <= 0)
return false;
remainingQuantity = Math.Max(0, previousQuantity - amount);
if (remainingQuantity == 0)
{
for (int match = matchingIndexes.Count - 1; match >= 0; match--)
items.RemoveAt(matchingIndexes[match]);
}
else
{
int index = matchingIndexes[0];
items[index].Quantity = remainingQuantity;
for (int match = matchingIndexes.Count - 1; match >= 1; match--)
items.RemoveAt(matchingIndexes[match]);
}
SaveConsumablesUnsafe(GetConsumablesPath(accountId), items);
return true;
}
}
private static List<AvatarItemOwnership> LoadAvatarCatalog()
{
string path = Path.Combine(
Program.dataDir,
"APIS",
"Items",
"AvatarItemsDefault.json");
if (!File.Exists(path))
return new List<AvatarItemOwnership>();
try
{
JsonNode? root = JsonNode.Parse(File.ReadAllText(path));
if (root is not JsonArray array)
return new List<AvatarItemOwnership>();
var results = new List<AvatarItemOwnership>();
foreach (JsonNode? node in array)
{
if (node is not JsonObject item)
continue;
long id = ReadLong(item, "AvatarItemId", "ItemId", "Id");
string descriptor = ReadString(
item,
"AvatarItemDesc",
"AvatarItemDescriptor",
"ItemDesc");
string name = ReadString(item, "FriendlyName", "Name");
if (id <= 0 && string.IsNullOrWhiteSpace(descriptor))
continue;
results.Add(new AvatarItemOwnership
{
AvatarItemId = id,
AvatarItemDesc = descriptor,
FriendlyName = string.IsNullOrWhiteSpace(name)
? descriptor
: name
});
}
return results
.GroupBy(item => string.IsNullOrWhiteSpace(item.AvatarItemDesc)
? $"id:{item.AvatarItemId}"
: $"desc:{item.AvatarItemDesc}",
StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.ToList();
}
catch
{
return new List<AvatarItemOwnership>();
}
}
private static List<ConsumableOwnership> LoadConsumableCatalog()
{
string path = Path.Combine(
Program.dataDir,
"APIS",
"Items",
"ConsumablesDefault.json");
if (!File.Exists(path))
return new List<ConsumableOwnership>();
try
{
JsonNode? root = JsonNode.Parse(File.ReadAllText(path));
if (root is not JsonArray array)
return new List<ConsumableOwnership>();
var results = new List<ConsumableOwnership>();
foreach (JsonNode? node in array)
{
if (node is not JsonObject item)
continue;
long id = ReadLong(item, "ConsumableItemId", "ItemId", "Id");
if (id <= 0 && item["Ids"] is JsonArray ids)
{
foreach (JsonNode? idNode in ids)
{
if (TryLong(idNode, out long candidate) && candidate > 0)
{
id = candidate;
break;
}
}
}
string descriptor = ReadString(
item,
"ConsumableItemDesc",
"ConsumableDesc",
"ItemDesc");
string name = ReadString(item, "FriendlyName", "Name");
if (id <= 0 && string.IsNullOrWhiteSpace(descriptor))
continue;
long configuredQuantity = ReadLong(
item,
"Count",
"Quantity",
"InitialCount");
int quantity = configuredQuantity > 0
? (int)Math.Min(100_000L, configuredQuantity)
: item["Ids"] is JsonArray itemIds
? Math.Max(1, itemIds.Count)
: 1;
results.Add(new ConsumableOwnership
{
ConsumableItemId = id,
ConsumableItemDesc = descriptor,
FriendlyName = string.IsNullOrWhiteSpace(name)
? descriptor
: name,
Quantity = quantity
});
}
return results
.GroupBy(item => string.IsNullOrWhiteSpace(item.ConsumableItemDesc)
? $"id:{item.ConsumableItemId}"
: $"desc:{item.ConsumableItemDesc}",
StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.ToList();
}
catch
{
return new List<ConsumableOwnership>();
}
}
private static bool IsSpecialAvatarItem(AvatarItemOwnership item) =>
string.Equals(item.FriendlyName, "Alpaca Shirt", StringComparison.OrdinalIgnoreCase);
private static bool SameAvatar(
AvatarItemOwnership left,
AvatarItemOwnership right) =>
(!string.IsNullOrWhiteSpace(left.AvatarItemDesc) &&
string.Equals(left.AvatarItemDesc, right.AvatarItemDesc, StringComparison.OrdinalIgnoreCase)) ||
(left.AvatarItemId > 0 && left.AvatarItemId == right.AvatarItemId);
private static bool SameConsumable(
ConsumableOwnership item,
string descriptor,
long consumableItemId) =>
(!string.IsNullOrWhiteSpace(descriptor) && string.Equals(
item.ConsumableItemDesc,
descriptor,
StringComparison.OrdinalIgnoreCase)) ||
(consumableItemId > 0 &&
item.ConsumableItemId == consumableItemId);
private static int SumConsumableQuantity(
IEnumerable<ConsumableOwnership> items)
{
long total = items.Sum(item => (long)Math.Max(0, item.Quantity));
return (int)Math.Min(100_000L, total);
}
private static void SaveAvatarItemsUnsafe(
string path,
List<AvatarItemOwnership> items) =>
File.WriteAllText(path, JsonSerializer.Serialize(items, WriteOptions));
private static void SaveConsumablesUnsafe(
string path,
List<ConsumableOwnership> items) =>
File.WriteAllText(path, JsonSerializer.Serialize(items, WriteOptions));
private static string ReadString(JsonObject item, params string[] names)
{
foreach (string name in names)
{
JsonNode? node = item[name];
if (node == null)
continue;
try
{
string? value = node.GetValue<string>();
if (!string.IsNullOrWhiteSpace(value))
return value.Trim();
}
catch
{
string value = node.ToJsonString().Trim('"');
if (!string.IsNullOrWhiteSpace(value) && value != "null")
return value;
}
}
return string.Empty;
}
private static long ReadLong(JsonObject item, params string[] names)
{
foreach (string name in names)
if (TryLong(item[name], out long value))
return value;
return 0;
}
private static bool TryLong(JsonNode? node, out long value)
{
value = 0;
if (node == null)
return false;
try
{
if (node is JsonValue jsonValue && jsonValue.TryGetValue(out value))
return true;
}
catch
{
}
return long.TryParse(node.ToJsonString().Trim('"'), out value);
}
}
}