605 lines
27 KiB
C#
605 lines
27 KiB
C#
using System.Buffers.Binary;
|
||
using System.IO;
|
||
using System.Text.Json;
|
||
using Ananta.SDK.Rpc;
|
||
using Ananta.SDK.Serialization;
|
||
using Ananta.Server.Configuration;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 钓鱼玩法(build 4229938)—— 从零接上服务端链路。
|
||
///
|
||
/// 原版链路(已用 dump.cs + 代理解码器 RPCDeserializeAuto.lua 坐实):
|
||
/// 走近钓点 → 客户端发 AskFishingSpotFullSync(fishGroupId) 拉鱼池
|
||
/// → 服务端回 SyncFishingSpotFull(Reader[335]) 推全量鱼池
|
||
/// → 交互跑 PlayFishingGameAction 小游戏
|
||
/// → 成功触发 OnFishingSuccess → 客户端发 AskFishingSuccess(fishGroupId, fishId) 记账
|
||
/// → 百科全书用 SyncFishAdd(Reader[277]) 登记。
|
||
///
|
||
/// 线格式铁律(UxContract.cs):复杂体只先写 TypeMark 字节(默认 0xFF)再按声明顺序写公共字段,
|
||
/// 客户端代理端 Auto.Reader[N] 的 N 只是解码索引、对线路字节无意义。所以下面每个 wire DTO 只要
|
||
/// 「字段顺序 / 类型」对齐代理的 Reader[335]/[338]/[277]/[347] 即可,类名无关。
|
||
///
|
||
/// 鱼池种子数据 100% 来自真实配置(FishingSpotConfig / FishingFishGroupConfig / FishingFishConfig),
|
||
/// 已复制进 ClientData/4229938/Configs/。客户端传什么 fishGroupId 都能接:
|
||
/// - 命中 FishingFishGroupConfig 的组号(1/2) → 用它的 InitGroup(短 id → 10105000+id)
|
||
/// - 命中 FishingSpotConfig 的 spot id(10105150…) → 用它的 InitGroup(已是全 id)
|
||
/// - 都不中 → 退回一套通用鱼,保证客户端永远有鱼可钓。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
// ── wire DTO:字段顺序严格对齐代理 Reader[335] / [338] / [277] / [347] ──────────────────────
|
||
[UxContract]
|
||
private sealed class FishingSpotFullSyncPayload4229938
|
||
{
|
||
public uint FishGroupId;
|
||
public List<FishingSpotFishWire4229938> Fishes = new();
|
||
public uint LastRefreshTime;
|
||
public uint LastResetTime;
|
||
public uint MaxCount;
|
||
}
|
||
|
||
[UxContract]
|
||
private sealed class FishingSpotFishWire4229938
|
||
{
|
||
public uint Id;
|
||
public uint FishConfigId;
|
||
public float Weight;
|
||
public float Length;
|
||
}
|
||
|
||
[UxContract]
|
||
private sealed class FishingFishAddWire4229938
|
||
{
|
||
public uint UniqueId;
|
||
public uint TemplateId;
|
||
public float Weight;
|
||
public float Length;
|
||
public bool IsPlaced;
|
||
}
|
||
|
||
[UxContract]
|
||
private sealed class FishingGearWire4229938
|
||
{
|
||
public uint UniqueId;
|
||
public uint TemplateId;
|
||
public uint RemainingDurability;
|
||
}
|
||
|
||
private static readonly Random Rng = new();
|
||
private const uint StarterRodConfigId = 10105100; // 入门鱼竿
|
||
|
||
// ═══════════════════════════ 常驻:拉鱼池 ═══════════════════════════
|
||
[Handler(MethodId.AskFishingSpotFullSync, HandlerPacketKind.Invoke)]
|
||
private async Task AskFishingSpotFullSync(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var fishGroupId = msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u;
|
||
var state = GetWorldState(msg.Context);
|
||
|
||
var (_, snap) = FishingCatalog4229938.EnsurePool(state, fishGroupId);
|
||
|
||
var payload = new FishingSpotFullSyncPayload4229938
|
||
{
|
||
FishGroupId = fishGroupId,
|
||
LastRefreshTime = FishingCatalog4229938.NowUnix(),
|
||
LastResetTime = FishingCatalog4229938.NowUnix(),
|
||
MaxCount = (uint)snap.Count,
|
||
};
|
||
foreach (var (id, fid, w, l) in snap)
|
||
payload.Fishes.Add(new FishingSpotFishWire4229938 { Id = id, FishConfigId = fid, Weight = w, Length = l });
|
||
|
||
// 返回值和 S2C 通知都带同样的 Reader[335] 形态:客户端两种都可能消费,两个都发(幂等)。
|
||
await conn.ReturnAsync(msg, payload);
|
||
await conn.NotifyAsync(MethodId.IGameToClient_SyncFishingSpotFull, payload);
|
||
}
|
||
|
||
// ═══════════════════════════ 常驻:钓上鱼记账 ═══════════════════════════
|
||
[Handler(MethodId.AskFishingSuccess, HandlerPacketKind.Invoke)]
|
||
private async Task AskFishingSuccess(Connection conn, UxRpcMessage msg)
|
||
{
|
||
uint fishGroupId = 0, fishId = 0;
|
||
if (msg.Body.Length >= 8)
|
||
{
|
||
fishGroupId = BinaryPrimitives.ReadUInt32LittleEndian(msg.Body);
|
||
fishId = BinaryPrimitives.ReadUInt32LittleEndian(msg.Body.AsSpan(4));
|
||
}
|
||
|
||
var state = GetWorldState(msg.Context);
|
||
if (TryRecordCatch(state, fishGroupId, fishId, out var uniqueId, out var templateId, out var weight, out var length))
|
||
{
|
||
await conn.NotifyAsync(MethodId.IGameToClient_SyncFishAdd, new FishingFishAddWire4229938
|
||
{
|
||
UniqueId = uniqueId,
|
||
TemplateId = templateId,
|
||
Weight = weight,
|
||
Length = length,
|
||
IsPlaced = false,
|
||
});
|
||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||
conn.Log.Info($"[FISHING] caught fishGroupId={fishGroupId} fishId={fishId} templateId={templateId} uniqueId={uniqueId}");
|
||
}
|
||
else
|
||
{
|
||
conn.Log.Warn($"[FISHING] AskFishingSuccess: no matching fish (group={fishGroupId}, fishId={fishId})");
|
||
}
|
||
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
}
|
||
|
||
private static bool TryRecordCatch(
|
||
WorldEntryState state, uint fishGroupId, uint fishId,
|
||
out uint uniqueId, out uint templateId, out float weight, out float length)
|
||
{
|
||
uniqueId = 0; templateId = 0; weight = 0; length = 0;
|
||
lock (state.SyncRoot)
|
||
{
|
||
if (state.Fishing.GameplayInfo.SpotPools.TryGetValue(fishGroupId, out var pool) &&
|
||
pool.Fishes.TryGetValue(fishId, out var fish))
|
||
{
|
||
uniqueId = state.Fishing.NextUniqueId++;
|
||
templateId = fish.FishConfigId;
|
||
weight = fish.Weight;
|
||
length = fish.Length;
|
||
state.Fishing.OrnamentalFishDict[uniqueId] = new Auto.FishInfo
|
||
{
|
||
UniqueId = uniqueId,
|
||
TemplateId = templateId,
|
||
Weight = weight,
|
||
Length = length,
|
||
};
|
||
pool.Fishes.Remove(fishId); // 钓走后从钓点鱼池移除(下次 SyncFishingSpotFull 反映)
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ═══════════════════════════ GM:发 / 收钓竿 ═══════════════════════════
|
||
[Handler(MethodId.GmAddFishingGear, HandlerPacketKind.Invoke)]
|
||
private async Task GmAddFishingGear(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var gearConfigId = msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u;
|
||
if (gearConfigId == 0) gearConfigId = StarterRodConfigId;
|
||
var state = GetWorldState(msg.Context);
|
||
|
||
uint uniqueId;
|
||
lock (state.SyncRoot)
|
||
{
|
||
uniqueId = state.Fishing.NextUniqueId++;
|
||
state.Fishing.GearDict[uniqueId] = new Auto.FishingGearInfo
|
||
{
|
||
UniqueId = uniqueId,
|
||
TemplateId = gearConfigId,
|
||
RemainingDurability = 100,
|
||
};
|
||
}
|
||
|
||
await conn.NotifyAsync(MethodId.IGameToClient_SyncFishingGearAdd, new FishingGearWire4229938
|
||
{
|
||
UniqueId = uniqueId,
|
||
TemplateId = gearConfigId,
|
||
RemainingDurability = 100,
|
||
});
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||
conn.Log.Info($"[FISHING] GmAddFishingGear granted gearConfigId={gearConfigId} uniqueId={uniqueId}");
|
||
}
|
||
|
||
[Handler(MethodId.GmRemoveFishingGear, HandlerPacketKind.Invoke)]
|
||
private async Task GmRemoveFishingGear(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var uniqueId = msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u;
|
||
var state = GetWorldState(msg.Context);
|
||
lock (state.SyncRoot)
|
||
state.Fishing.GearDict.Remove(uniqueId);
|
||
|
||
// SyncFishingGearRemove 是裸 uint(代理 ReadUInt32),用 NotifyRawAsync 发原始 4 字节。
|
||
await conn.NotifyRawAsync(MethodId.IGameToClient_SyncFishingGearRemove, BitConverter.GetBytes(uniqueId));
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||
}
|
||
|
||
// ═══════════════════════════ GM:钓点 / 鱼池维护 ═══════════════════════════
|
||
[Handler(MethodId.GmInitFishingSpot, HandlerPacketKind.Invoke)]
|
||
private async Task GmInitFishingSpot(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var spotId = msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u;
|
||
var state = GetWorldState(msg.Context);
|
||
FishingCatalog4229938.EnsurePool(state, spotId);
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
conn.Log.Info($"[FISHING] GmInitFishingSpot seeded pool for spot/group {spotId}");
|
||
}
|
||
|
||
[Handler(MethodId.GmSpawnFishingSpotFish, HandlerPacketKind.Invoke)]
|
||
private async Task GmSpawnFishingSpotFish(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var spotId = msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u;
|
||
var state = GetWorldState(msg.Context);
|
||
uint spawned = 0;
|
||
lock (state.SyncRoot)
|
||
{
|
||
var pool = FishingCatalog4229938.GetOrCreatePool(state, spotId);
|
||
uint instanceId = 1;
|
||
while (pool.Fishes.ContainsKey(instanceId)) instanceId++;
|
||
spawned = FishingCatalog4229938.PickFishConfigId();
|
||
pool.Fishes[instanceId] = new Auto.PlayerFishingSpotFish
|
||
{
|
||
Id = instanceId,
|
||
FishConfigId = spawned,
|
||
Weight = (float)Math.Round(0.3 + Rng.NextDouble() * 5.7, 2),
|
||
Length = (float)Math.Round(8 + Rng.NextDouble() * 82, 1),
|
||
SpawnTime = 0,
|
||
};
|
||
}
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
conn.Log.Info($"[FISHING] GmSpawnFishingSpotFish spot={spotId} fishConfigId={spawned} (client refreshes on next AskFishingSpotFullSync)");
|
||
}
|
||
|
||
[Handler(MethodId.GmResetAllFishingSpots, HandlerPacketKind.Invoke)]
|
||
private async Task GmResetAllFishingSpots(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var state = GetWorldState(msg.Context);
|
||
lock (state.SyncRoot)
|
||
{
|
||
state.Fishing.GameplayInfo.SpotPools.Clear();
|
||
state.Fishing.GameplayInfo.SpotLastRefreshTime.Clear();
|
||
state.Fishing.GameplayInfo.SpotLastResetTime.Clear();
|
||
}
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||
conn.Log.Info("[FISHING] GmResetAllFishingSpots cleared all spot pools");
|
||
}
|
||
|
||
[Handler(MethodId.GmClearFishing, HandlerPacketKind.Invoke)]
|
||
private async Task GmClearFishing(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var state = GetWorldState(msg.Context);
|
||
lock (state.SyncRoot)
|
||
{
|
||
state.Fishing.GearDict.Clear();
|
||
state.Fishing.ConsumableFishDict.Clear();
|
||
state.Fishing.OrnamentalFishDict.Clear();
|
||
state.Fishing.NextUniqueId = 0;
|
||
state.Fishing.GameplayInfo.SpotPools.Clear();
|
||
state.Fishing.GameplayInfo.SpotLastRefreshTime.Clear();
|
||
state.Fishing.GameplayInfo.SpotLastResetTime.Clear();
|
||
}
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||
conn.Log.Info("[FISHING] GmClearFishing wiped all fishing state");
|
||
}
|
||
|
||
// ═══════════════════════════ GM:百科全书灌全鱼 ═══════════════════════════
|
||
[Handler(MethodId.GmAddAllFishes, HandlerPacketKind.Invoke)]
|
||
private async Task GmAddAllFishes(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var state = GetWorldState(msg.Context);
|
||
var fishIds = FishingCatalog4229938.AllFishIds;
|
||
uint pushed = 0;
|
||
foreach (var fishId in fishIds)
|
||
{
|
||
uint uid;
|
||
lock (state.SyncRoot) uid = state.Fishing.NextUniqueId++;
|
||
await conn.NotifyAsync(MethodId.IGameToClient_SyncFishAdd, new FishingFishAddWire4229938
|
||
{
|
||
UniqueId = uid,
|
||
TemplateId = fishId,
|
||
Weight = 0,
|
||
Length = 0,
|
||
IsPlaced = false,
|
||
});
|
||
pushed++;
|
||
}
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
conn.Log.Info($"[FISHING] GmAddAllFishes pushed {pushed} fish to encyclopedia");
|
||
}
|
||
|
||
// ═══════════════════════════ GM:强制记一笔钓获 ═══════════════════════════
|
||
[Handler(MethodId.GmOnFishingSuccess, HandlerPacketKind.Invoke)]
|
||
private async Task GmOnFishingSuccess(Connection conn, UxRpcMessage msg)
|
||
{
|
||
uint spotId = 0, fishId = 0;
|
||
if (msg.Body.Length >= 8)
|
||
{
|
||
spotId = BinaryPrimitives.ReadUInt32LittleEndian(msg.Body);
|
||
fishId = BinaryPrimitives.ReadUInt32LittleEndian(msg.Body.AsSpan(4));
|
||
}
|
||
var state = GetWorldState(msg.Context);
|
||
if (TryRecordCatch(state, spotId, fishId, out var uniqueId, out var templateId, out var weight, out var length))
|
||
{
|
||
await conn.NotifyAsync(MethodId.IGameToClient_SyncFishAdd, new FishingFishAddWire4229938
|
||
{
|
||
UniqueId = uniqueId,
|
||
TemplateId = templateId,
|
||
Weight = weight,
|
||
Length = length,
|
||
IsPlaced = false,
|
||
});
|
||
PlayerProgressStore.Save(state, conn.Log, quiet: true);
|
||
conn.Log.Info($"[FISHING] GmOnFishingSuccess recorded spot={spotId} fishId={fishId} templateId={templateId}");
|
||
}
|
||
else
|
||
{
|
||
conn.Log.Warn($"[FISHING] GmOnFishingSuccess: no matching fish (spot={spotId}, fishId={fishId})");
|
||
}
|
||
await conn.ReturnEmptyOkAsync(msg);
|
||
}
|
||
|
||
// ═══════════════════════════ GM:调试查看(有 string 返回) ═══════════════════════════
|
||
// ⛑⛑ 2026-09-29 修正:这两条**不是 void**,客户端
|
||
// `midToReturnMessageReader[GmViewFishingSpot/GmViewFishingInfo]` 都是
|
||
// `reader.ReadString(reader)`(裸 string)。原来 `ReturnEmptyOkAsync` 是 **0 字节载荷**
|
||
// (Connection.cs:32 `Array.Empty<byte>()`)⇒ 客户端读空 buffer,GM 面板什么都看不到。
|
||
// 实测 [FISHING] 一轮 9 次(一直在点)。现在把已经拼好的 dump **真的回给客户端**。
|
||
// wire:`UxWriter.UxString` —— null 写 0x00,有值写 `VarInt(len+1)` + UTF8(空串 = 0x01)。
|
||
// ⛑ 回滚:把 return 换回 `await conn.ReturnEmptyOkAsync(msg)` + 签名改回 `async Task`。
|
||
[Handler(MethodId.GmViewFishingSpot, HandlerPacketKind.Invoke)]
|
||
private Task GmViewFishingSpot(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var spotId = msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u;
|
||
var state = GetWorldState(msg.Context);
|
||
string dump;
|
||
lock (state.SyncRoot)
|
||
{
|
||
if (state.Fishing.GameplayInfo.SpotPools.TryGetValue(spotId, out var pool))
|
||
dump = $"spot/group {spotId}: {pool.Fishes.Count} fish -> " +
|
||
string.Join(",", pool.Fishes.Values.Select(f => f.FishConfigId.ToString()));
|
||
else
|
||
dump = $"spot/group {spotId}: no pool yet";
|
||
}
|
||
conn.Log.Info($"[FISHING] GmViewFishingSpot {dump}");
|
||
var w = new UxWriter();
|
||
w.UxString(dump);
|
||
return conn.ReturnRawAsync(msg, w.ToArray());
|
||
}
|
||
|
||
[Handler(MethodId.GmViewFishingInfo, HandlerPacketKind.Invoke)]
|
||
private Task GmViewFishingInfo(Connection conn, UxRpcMessage msg)
|
||
{
|
||
var state = GetWorldState(msg.Context);
|
||
string dump;
|
||
lock (state.SyncRoot)
|
||
dump = $"gear={state.Fishing.GearDict.Count} ornamental={state.Fishing.OrnamentalFishDict.Count} " +
|
||
$"consumable={state.Fishing.ConsumableFishDict.Count} spots={state.Fishing.GameplayInfo.SpotPools.Count} " +
|
||
$"nextUid={state.Fishing.NextUniqueId}";
|
||
conn.Log.Info($"[FISHING] GmViewFishingInfo {dump}");
|
||
var w = new UxWriter();
|
||
w.UxString(dump);
|
||
return conn.ReturnRawAsync(msg, w.ToArray());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 钓鱼配置目录(懒加载一次)。读真实 JSON(FishingSpotConfig / FishingFishGroupConfig /
|
||
/// FishingFishConfig),找不到文件就退化为通用鱼池,绝不让钓鱼链路崩。
|
||
/// </summary>
|
||
internal static class FishingCatalog4229938
|
||
{
|
||
private sealed record FishingInitGroup(uint FishId, uint Num);
|
||
private sealed record FishingSpotRec(uint Id, List<FishingInitGroup> InitGroup);
|
||
private sealed record FishingGroupRec(uint Id, uint MaxCount, List<FishingInitGroup> InitGroup);
|
||
|
||
private static readonly Dictionary<uint, FishingSpotRec> Spots = new();
|
||
private static readonly Dictionary<uint, FishingGroupRec> Groups = new();
|
||
private static readonly List<uint> AllFishIdsField = new();
|
||
|
||
/// <summary> fishId → (NormalWeight 克, NormalLength 厘米),取自 FishingFishConfig,用于生成贴近原版的体重/体长。 </summary>
|
||
private static readonly Dictionary<uint, (float WeightKg, float LengthCm)> FishBody = new();
|
||
|
||
private static readonly Random Rng = new();
|
||
private static readonly object Gate = new();
|
||
|
||
static FishingCatalog4229938()
|
||
{
|
||
try
|
||
{
|
||
var root = PrivateServerConfigStore.ResolveProjectPath(
|
||
PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||
LoadSpots(Path.Combine(root, "FishingSpotConfig.json"));
|
||
LoadGroups(Path.Combine(root, "FishingFishGroupConfig.json"));
|
||
LoadFish(Path.Combine(root, "FishingFishConfig.json"));
|
||
Console.WriteLine(
|
||
$"[FISHING] catalog loaded: spots={Spots.Count} groups={Groups.Count} fish={AllFishIds.Count} bodies={FishBody.Count}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"[FISHING] config load failed ({ex.GetType().Name}: {ex.Message}) — falling back to generic pools");
|
||
}
|
||
}
|
||
|
||
internal static List<uint> AllFishIds => AllFishIdsField;
|
||
|
||
internal static uint NowUnix() => (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||
|
||
internal static (Auto.PlayerFishingSpotPool Pool, List<(uint Id, uint FishConfigId, float Weight, float Length)> Snapshot)
|
||
EnsurePool(WorldEntryState state, uint fishGroupId)
|
||
{
|
||
lock (state.SyncRoot)
|
||
{
|
||
if (!state.Fishing.GameplayInfo.SpotPools.TryGetValue(fishGroupId, out var pool))
|
||
{
|
||
pool = BuildPool(fishGroupId);
|
||
state.Fishing.GameplayInfo.SpotPools[fishGroupId] = pool;
|
||
state.Fishing.GameplayInfo.SpotLastRefreshTime[fishGroupId] = NowUnix();
|
||
state.Fishing.GameplayInfo.SpotLastResetTime[fishGroupId] = NowUnix();
|
||
}
|
||
var snap = new List<(uint, uint, float, float)>();
|
||
foreach (var f in pool.Fishes.Values)
|
||
snap.Add((f.Id, f.FishConfigId, f.Weight, f.Length));
|
||
return (pool, snap);
|
||
}
|
||
}
|
||
|
||
internal static Auto.PlayerFishingSpotPool GetOrCreatePool(WorldEntryState state, uint fishGroupId)
|
||
{
|
||
lock (state.SyncRoot)
|
||
{
|
||
if (!state.Fishing.GameplayInfo.SpotPools.TryGetValue(fishGroupId, out var pool))
|
||
{
|
||
pool = BuildPool(fishGroupId);
|
||
state.Fishing.GameplayInfo.SpotPools[fishGroupId] = pool;
|
||
state.Fishing.GameplayInfo.SpotLastRefreshTime[fishGroupId] = NowUnix();
|
||
state.Fishing.GameplayInfo.SpotLastResetTime[fishGroupId] = NowUnix();
|
||
}
|
||
return pool;
|
||
}
|
||
}
|
||
|
||
internal static uint PickFishConfigId()
|
||
{
|
||
lock (Gate)
|
||
{
|
||
if (AllFishIds.Count == 0) return 10105001u;
|
||
return AllFishIds[Rng.Next(AllFishIds.Count)];
|
||
}
|
||
}
|
||
|
||
private static Auto.PlayerFishingSpotPool BuildPool(uint fishGroupId)
|
||
{
|
||
var pool = new Auto.PlayerFishingSpotPool();
|
||
var entries = new List<(uint FishId, uint Count)>();
|
||
uint maxCount = 20;
|
||
|
||
if (Groups.TryGetValue(fishGroupId, out var g))
|
||
{
|
||
foreach (var ig in g.InitGroup)
|
||
entries.Add((NormalizeFishId(ig.FishId), ig.Num));
|
||
maxCount = g.MaxCount;
|
||
}
|
||
else if (Spots.TryGetValue(fishGroupId, out var s))
|
||
{
|
||
foreach (var ig in s.InitGroup)
|
||
entries.Add((ig.FishId, ig.Num));
|
||
maxCount = (uint)entries.Sum(e => (int)e.Count);
|
||
}
|
||
else
|
||
{
|
||
// 兜底:客户端传了一个谁都不认的 id —— 给一套通用鱼,保证能钓。
|
||
foreach (var fid in new[] { 10105001u, 10105002u, 10105003u, 10105004u, 10105005u })
|
||
entries.Add((fid, 4));
|
||
maxCount = 20;
|
||
}
|
||
|
||
uint instanceId = 1;
|
||
foreach (var (fishId, count) in entries)
|
||
{
|
||
// 还原优先:体重/体长围绕 FishingFishConfig 的 NormalWeight(克)/NormalLength(厘米) ±20% 抖动,
|
||
// 没有该鱼配置时退回通用区间。
|
||
float baseW = 1f, baseL = 30f;
|
||
lock (Gate)
|
||
{
|
||
if (FishBody.TryGetValue(fishId, out var body)) { baseW = body.WeightKg; baseL = body.LengthCm; }
|
||
}
|
||
for (uint i = 0; i < count && instanceId <= 200; i++)
|
||
{
|
||
float weight, length;
|
||
lock (Gate)
|
||
{
|
||
weight = (float)Math.Round(baseW * (0.8 + Rng.NextDouble() * 0.4), 2);
|
||
length = (float)Math.Round(baseL * (0.8 + Rng.NextDouble() * 0.4), 1);
|
||
}
|
||
pool.Fishes[instanceId] = new Auto.PlayerFishingSpotFish
|
||
{
|
||
Id = instanceId,
|
||
FishConfigId = fishId,
|
||
Weight = weight,
|
||
Length = length,
|
||
SpawnTime = 0,
|
||
};
|
||
instanceId++;
|
||
}
|
||
}
|
||
return pool;
|
||
}
|
||
|
||
/// <summary>FishingFishGroupConfig 用的是短 id(2/10/3/7…),补成全 id(10105002…);已是全 id 的不动。</summary>
|
||
private static uint NormalizeFishId(uint raw) => raw < 1000 ? 10105000u + raw : raw;
|
||
|
||
/// <summary>
|
||
/// 宽容 uint 读取:客户端导出的 JSON 里数字字段混用 number / string(如 "FishId": "2"、
|
||
/// "Id": 10105001),直接 GetUInt32() 会在 string 上抛 InvalidOperationException。
|
||
/// </summary>
|
||
private static uint ReadUIntEl(System.Text.Json.JsonElement el)
|
||
{
|
||
return el.ValueKind switch
|
||
{
|
||
System.Text.Json.JsonValueKind.Number => el.GetUInt32(),
|
||
System.Text.Json.JsonValueKind.String => uint.TryParse(el.GetString(), out var v) ? v : 0u,
|
||
_ => 0u,
|
||
};
|
||
}
|
||
|
||
private static void LoadSpots(string path)
|
||
{
|
||
if (!File.Exists(path)) return;
|
||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||
if (!doc.RootElement.TryGetProperty("records", out var records)) return;
|
||
foreach (var r in records.EnumerateArray())
|
||
{
|
||
if (!r.TryGetProperty("Id", out var idEl)) continue;
|
||
var id = ReadUIntEl(idEl);
|
||
var init = new List<FishingInitGroup>();
|
||
if (r.TryGetProperty("InitGroup", out var ig))
|
||
foreach (var e in ig.EnumerateArray())
|
||
{
|
||
var fid = NormalizeFishId(ReadUIntEl(e.GetProperty("FishId")));
|
||
var num = e.TryGetProperty("Num", out var n) ? Math.Max(1u, ReadUIntEl(n)) : 1u;
|
||
init.Add(new FishingInitGroup(fid, num));
|
||
}
|
||
Spots[id] = new FishingSpotRec(id, init);
|
||
}
|
||
}
|
||
|
||
private static void LoadGroups(string path)
|
||
{
|
||
if (!File.Exists(path)) return;
|
||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||
if (!doc.RootElement.TryGetProperty("records", out var records)) return;
|
||
foreach (var r in records.EnumerateArray())
|
||
{
|
||
if (!r.TryGetProperty("Id", out var idEl)) continue;
|
||
var id = ReadUIntEl(idEl);
|
||
var max = r.TryGetProperty("MaxCount", out var m) ? Math.Max(1u, ReadUIntEl(m)) : 20u;
|
||
var init = new List<FishingInitGroup>();
|
||
if (r.TryGetProperty("InitGroup", out var ig))
|
||
foreach (var e in ig.EnumerateArray())
|
||
{
|
||
var fid = NormalizeFishId(ReadUIntEl(e.GetProperty("FishId")));
|
||
var num = e.TryGetProperty("Num", out var n) ? Math.Max(1u, ReadUIntEl(n)) : 1u;
|
||
init.Add(new FishingInitGroup(fid, num));
|
||
}
|
||
Groups[id] = new FishingGroupRec(id, max, init);
|
||
}
|
||
}
|
||
|
||
private static void LoadFish(string path)
|
||
{
|
||
if (!File.Exists(path)) return;
|
||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||
if (!doc.RootElement.TryGetProperty("records", out var records)) return;
|
||
foreach (var r in records.EnumerateArray())
|
||
{
|
||
if (!r.TryGetProperty("Id", out var idEl)) continue;
|
||
var id = ReadUIntEl(idEl);
|
||
if (id == 0) continue;
|
||
AllFishIds.Add(id);
|
||
// NormalWeight 单位是克(草鱼 5000 ≈ 5kg)、NormalLength 是厘米(60)。
|
||
float wKg = 1f, lCm = 30f;
|
||
if (r.TryGetProperty("NormalWeight", out var wEl) && wEl.ValueKind is System.Text.Json.JsonValueKind.Number or System.Text.Json.JsonValueKind.String)
|
||
{
|
||
var g = ReadUIntEl(wEl);
|
||
if (g > 0) wKg = g / 1000f;
|
||
}
|
||
if (r.TryGetProperty("NormalLength", out var lEl) && lEl.ValueKind is System.Text.Json.JsonValueKind.Number or System.Text.Json.JsonValueKind.String)
|
||
{
|
||
var cm = ReadUIntEl(lEl);
|
||
if (cm > 0) lCm = cm;
|
||
}
|
||
FishBody[id] = (wKg, lCm);
|
||
}
|
||
}
|
||
}
|