yuanshen/Yuanshen.Gameserver/Game/DropTable.cs
wisedev 62205c8062 Chests, ore and artifact drops
Gadgets are streamed alongside monsters now, so chests and ore nodes show up in
the world and can be interacted with. The two flows differ: for a chest the
client sends the finish request itself and the loot is granted on that one, for
ore it sends nothing more so the server has to close everything on the first
request.

Artifacts used to be silently dropped on the floor because the item ids that
actually roll were not in the reliquary table at all. Added the missing rows and
gave them real main stat and substat rolls instead of the fixed four.

Dropped items survive a relogin. The inventory is still rebuilt from the tables
on every login, so the trick is to remember where the default set ends and only
persist what was found in the world. Gacha and shop weapons were disappearing
the same way and are fixed by the same change.
2026-09-04 10:11:21 +03:00

185 lines
6.2 KiB
C#

using System.Text.Json;
namespace Yuanshen.Gameserver.Game;
internal readonly record struct DropEntry(uint ItemId, uint MinCount, uint MaxCount, double Chance, int Rolls, int Pool);
internal sealed class DropTable
{
private readonly Dictionary<uint, Dictionary<uint, uint>> _monsters = new();
private readonly Dictionary<uint, List<DropEntry>> _drops = new();
private readonly Dictionary<uint, string> _itemTypes = new();
private readonly Dictionary<uint, List<(uint MaxLevel, uint DropId)>> _subfields = new();
public DropTable()
{
Load("monster_drops.json");
Load("gadget_drops.json");
}
public uint SubfieldDropId(uint subfieldId, uint level)
{
if (!_subfields.TryGetValue(subfieldId, out List<(uint MaxLevel, uint DropId)>? bands) || bands.Count == 0)
return 0;
foreach ((uint maxLevel, uint dropId) in bands.OrderBy(band => band.MaxLevel))
{
if (level <= maxLevel)
return dropId;
}
return bands[^1].DropId;
}
public bool IsEmpty => _drops.Count == 0;
public int MonsterCount => _monsters.Count;
public string TypeOf(uint itemId) => _itemTypes.GetValueOrDefault(itemId, "material");
public uint DropIdFor(uint monsterId, uint level)
{
if (!_monsters.TryGetValue(monsterId, out Dictionary<uint, uint>? byLevel) || byLevel.Count == 0)
return 0;
if (byLevel.TryGetValue(level, out uint exact))
return exact;
uint best = 0;
uint bestLevel = 0;
foreach ((uint candidate, uint dropId) in byLevel)
{
if (candidate <= level && candidate >= bestLevel)
{
bestLevel = candidate;
best = dropId;
}
}
return best != 0 ? best : byLevel.OrderBy(pair => pair.Key).First().Value;
}
public Dictionary<uint, uint> Roll(uint dropId, Random random)
{
Dictionary<uint, uint> result = new();
if (!_drops.TryGetValue(dropId, out List<DropEntry>? entries))
return result;
foreach (DropEntry entry in entries.Where(entry => entry.Pool == 0))
{
for (int attempt = 0; attempt < entry.Rolls; attempt++)
{
if (random.NextDouble() < entry.Chance)
Add(result, entry, random);
}
}
foreach (IGrouping<int, DropEntry> group in entries.Where(entry => entry.Pool != 0).GroupBy(entry => entry.Pool))
{
List<DropEntry> members = group.ToList();
for (int attempt = 0; attempt < members[0].Rolls; attempt++)
{
double roll = random.NextDouble();
double accumulated = 0;
foreach (DropEntry member in members)
{
accumulated += member.Chance;
if (roll >= accumulated)
continue;
Add(result, member, random);
break;
}
}
}
return result;
}
private static void Add(Dictionary<uint, uint> result, DropEntry entry, Random random)
{
uint count = entry.MaxCount > entry.MinCount
? (uint)random.Next((int)entry.MinCount, (int)entry.MaxCount + 1)
: entry.MinCount;
if (count != 0)
result[entry.ItemId] = result.GetValueOrDefault(entry.ItemId) + count;
}
private void Load(string fileName)
{
string path = Path.Combine(AppContext.BaseDirectory, "assets", "drops", fileName);
if (!File.Exists(path))
return;
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
JsonElement root = document.RootElement;
if (root.TryGetProperty("subfield", out JsonElement subfields))
{
foreach (JsonProperty subfield in subfields.EnumerateObject())
{
if (!uint.TryParse(subfield.Name, out uint subfieldId))
continue;
List<(uint, uint)> bands = new();
foreach (JsonElement band in subfield.Value.EnumerateArray())
bands.Add((band.GetProperty("maxLevel").GetUInt32(), band.GetProperty("dropId").GetUInt32()));
if (bands.Count != 0)
_subfields[subfieldId] = bands;
}
}
foreach (JsonProperty monster in root.TryGetProperty("monsters", out JsonElement monsterRoot)
? monsterRoot.EnumerateObject()
: default)
{
if (!uint.TryParse(monster.Name, out uint monsterId))
continue;
Dictionary<uint, uint> byLevel = new();
foreach (JsonProperty level in monster.Value.EnumerateObject())
{
if (uint.TryParse(level.Name, out uint parsedLevel))
byLevel[parsedLevel] = level.Value.GetUInt32();
}
if (byLevel.Count != 0)
_monsters[monsterId] = byLevel;
}
foreach (JsonProperty drop in root.TryGetProperty("drops", out JsonElement dropRoot) ? dropRoot.EnumerateObject() : default)
{
if (!uint.TryParse(drop.Name, out uint dropId))
continue;
List<DropEntry> entries = new();
foreach (JsonElement entry in drop.Value.EnumerateArray())
{
entries.Add(new DropEntry(
entry.GetProperty("itemId").GetUInt32(),
entry.GetProperty("minCount").GetUInt32(),
entry.GetProperty("maxCount").GetUInt32(),
entry.GetProperty("chance").GetDouble(),
entry.TryGetProperty("rolls", out JsonElement rolls) ? Math.Max(rolls.GetInt32(), 1) : 1,
entry.TryGetProperty("pool", out JsonElement pool) ? pool.GetInt32() : 0));
}
_drops[dropId] = entries;
}
foreach (JsonProperty item in root.TryGetProperty("items", out JsonElement itemRoot) ? itemRoot.EnumerateObject() : default)
{
if (uint.TryParse(item.Name, out uint itemId))
_itemTypes[itemId] = item.Value.GetProperty("type").GetString() ?? "material";
}
}
}