出鱼协议改为 OnFishResync 全量替换(废弃 OnFishSpawn(type=sync));socket.html 同步更新;后台 user/list.html 更新;version 2.11.11
This commit is contained in:
@@ -88,9 +88,9 @@
|
||||
<button type="button" class="layui-btn layui-btn-sm layui-btn-primary" id="btn-reset">
|
||||
<i class="layui-icon layui-icon-refresh"></i> 重置
|
||||
</button>
|
||||
<button type="button" class="layui-btn layui-btn-sm layui-btn-warm" id="btn-sync-level">
|
||||
<!--<button type="button" class="layui-btn layui-btn-sm layui-btn-warm" id="btn-sync-level">
|
||||
<i class="layui-icon layui-icon-refresh-3"></i> 同步等级
|
||||
</button>
|
||||
</button>-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" style="margin-bottom:0;">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<script>
|
||||
layui.config({
|
||||
base: './dist/' //指定 layuiAdmin 项目路径
|
||||
, version: '2.11.10'
|
||||
, version: '2.11.11'
|
||||
}).use('index', function () {
|
||||
var layer = layui.layer, admin = layui.admin;
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
|
||||
expireAt: number, // 毫秒
|
||||
remainingSeconds: int
|
||||
}]
|
||||
}</pre></td><td>调用者</td><td>加入房间/断线重连/取消挂机返回房间信息。先用 activeFishes 初始化鱼群;服务端约 300ms 后还会向当前玩家补发同一批 OnFishSpawn(init),前端须按 fishId 去重。</td></tr>
|
||||
}</pre></td><td>调用者</td><td>加入房间/断线重连/取消挂机返回房间信息,活跃鱼随本事件在 <code>activeFishes</code> 中返回。<b>注意</b>:重连 / 取消挂机 / 挂起恢复场景会另行下发独立事件 <code>OnFishResync</code>(全量替换语义),前端按该事件重建鱼群。</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> SyncPlayerGold</td><td><code>long</code></td><td>调用者</td><td>金币同步(金币≤0时触发)</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> SendPlayerInfoResult</td><td><code>long?</code></td><td>调用者</td><td>玩家Gold值</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> GetRoomInfoResult</td><td><code>RedisGameRoom?</code></td><td>调用者</td><td>房间信息查询结果</td></tr>
|
||||
@@ -288,7 +288,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
|
||||
position: int, showWaiting: bool? }</pre></td><td>房间组</td><td>座位变更(加入/离开)</td></tr>
|
||||
|
||||
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> OnFishSpawn<span class="tag-new">NEW</span></td><td><pre>{
|
||||
type: "init" | "spawn" | "sync", // init=进房补发,spawn=定时出鱼,sync=主动同步
|
||||
type: "init" | "spawn", // init=进房补发,spawn=定时出鱼(sync 已废弃:重连/恢复改走 OnFishResync)
|
||||
timestamp: number, // 服务器当前时间戳(毫秒)
|
||||
fish: [{
|
||||
fishId: number, // 普通鱼ID;鱼阵时为首条普通鱼ID(兼容旧客户端)
|
||||
@@ -300,7 +300,23 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
|
||||
expireAt: number, // 鱼过期时间戳(毫秒)
|
||||
remainingSeconds: int // 鱼剩余存活秒数
|
||||
}]
|
||||
}</pre></td><td>房间组(init/spawn)/调用者(sync)</td><td><code>fishIds</code> 在首次出鱼、定时出鱼、入房及重连同步中都必须提供。普通鱼为单元素集合 <code>[fishId]</code>;鱼阵按鱼阵配置的生成顺序使用 ID 集合。鱼阵的 <code>fishNumber</code> 为空,鱼信息由 <code>fishArrayNumber</code> 对应的本地配置确定。击中时把对应 ID 作为 KillFish 的 <code>fihsId</code> 原样传回。</td></tr>
|
||||
}</pre></td><td>房间组</td><td><code>fishIds</code> 在首次出鱼、定时出鱼、入房及重连同步中都必须提供。普通鱼为单元素集合 <code>[fishId]</code>;鱼阵按鱼阵配置的生成顺序使用 ID 集合。鱼阵的 <code>fishNumber</code> 为空,鱼信息由 <code>fishArrayNumber</code> 对应的本地配置确定。击中时把对应 ID 作为 KillFish 的 <code>fihsId</code> 原样传回。</td></tr>
|
||||
<tr class="row-new"><td><span class="tag tag-sc">S→C</span> OnFishResync<span class="tag-new">NEW</span></td><td><pre>{
|
||||
roomId: string, // 房间ID
|
||||
timestamp: number, // 服务器当前时间戳(毫秒)
|
||||
serverTime: string, // "yyyy-MM-dd HH:mm:ss.fff"
|
||||
count: number, // 鱼数量(鱼阵按阵型合并为一条)
|
||||
fish: [{ // 与 OnFishSpawn 相同的鱼结构
|
||||
fishId: number,
|
||||
fishIds: number[],
|
||||
fishNumber: string,
|
||||
routeId: number,
|
||||
fishArrayNumber: string,
|
||||
createdAt: number, // 毫秒
|
||||
expireAt: number, // 毫秒
|
||||
remainingSeconds: int
|
||||
}]
|
||||
}</pre></td><td>调用者</td><td><b>全量替换语义</b>:客户端应<b>先清空本地旧鱼</b>,再按 <code>fish</code> 列表重建(不要把本事件当作增量补发)。用于断线重连、挂起恢复等场景,<b>替代原 <code>OnFishSpawn(type=sync)</code></b>;<code>type=sync</code> 已废弃。</td></tr>
|
||||
<tr><td><span class="tag tag-sc">S→C</span> KillNotice</td><td><pre>{
|
||||
FishId: string, // 鱼编号
|
||||
userName: string, // 玩家名
|
||||
@@ -562,7 +578,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
|
||||
<li>无 -> 无操作,等待后续指令</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>返回结果:RoomInfo(首个真人注册后的首次鱼群放入 activeFishes,约 300ms 后向当前玩家补发 OnFishSpawn(type=init))</li>
|
||||
<li>返回结果:RoomInfo(首个真人注册后的首次鱼群放入 activeFishes;重连/恢复场景另行下发 OnFishResync 全量替换)</li>
|
||||
</ol>
|
||||
|
||||
<h4>5. CancelPlayerAfk 流程</h4>
|
||||
@@ -602,7 +618,7 @@ ul { padding-left: 20px; } li { margin: 4px 0; font-size: 13px; }
|
||||
<ul>
|
||||
<li>首个真人进房 -> RegisterRoom -> 启动定时器持续出鱼</li>
|
||||
<li>定时出鱼 -> OnFishSpawn(type=spawn) 广播房间组</li>
|
||||
<li>后续真人进房/重连/取消挂机 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回,并约 300ms 后补发 OnFishSpawn(type=init)</li>
|
||||
<li>后续真人进房 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回;重连 / 取消挂机 / 挂起恢复 -> 额外下发独立事件 OnFishResync(全量替换)</li>
|
||||
<li>最后一个真人退出/断线 -> UnregisterRoom -> 停止定时器 + 清除活跃鱼</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -63,15 +63,75 @@ namespace Service.Fishing
|
||||
_subscriber.Publish("fishgame:global", msg);
|
||||
}
|
||||
|
||||
public async Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null)
|
||||
public async Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null, bool sendInitialFish = true)
|
||||
{
|
||||
object payload;
|
||||
List<object> compatibilityFishPayload = null;
|
||||
List<object> initialFishPayload = null;
|
||||
if (activeFishes != null && activeFishes.Count > 0)
|
||||
{
|
||||
initialFishPayload = BuildActiveFishPayload(activeFishes);
|
||||
|
||||
payload = new
|
||||
{
|
||||
roomInfo.RoomId,
|
||||
roomInfo.RoomName,
|
||||
roomInfo.GameType,
|
||||
roomInfo.RoomLevel,
|
||||
roomInfo.RoomPartition,
|
||||
roomInfo.RoomNumber,
|
||||
roomInfo.MaxPlayerCount,
|
||||
roomInfo.CurrentOnlinePlayerCount,
|
||||
roomInfo.RobotCount,
|
||||
roomInfo.Players,
|
||||
roomInfo.CreateUserId,
|
||||
roomInfo.SystemFactor,
|
||||
activeFishes = initialFishPayload
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = roomInfo;
|
||||
}
|
||||
await _hubSend.SendUserCIdAsync(cid, "RoomInfo", payload);
|
||||
if (sendInitialFish && initialFishPayload != null && initialFishPayload.Count > 0)
|
||||
{
|
||||
_ = SendInitialFishAfterSceneReady(cid, roomInfo.RoomId, initialFishPayload);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补发场上活鱼(挂起恢复 / 重连专用独立协议):
|
||||
/// 发到独立事件 OnFishResync,客户端按"全量替换"处理(先清本地旧鱼,再按本列表重建)。
|
||||
/// fish 列表口径与 RoomInfo.activeFishes / 首屏 init 完全一致,都带服务端原始的 createdAt。
|
||||
/// </summary>
|
||||
public async Task FishResyncNoticeAsync(string cid, string roomId, List<ActiveFish> activeFishes)
|
||||
{
|
||||
var fish = activeFishes == null || activeFishes.Count == 0
|
||||
? new List<object>()
|
||||
: BuildActiveFishPayload(activeFishes);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var activeFishPayload = activeFishes
|
||||
.Where(f => string.IsNullOrWhiteSpace(f.FishArrayNumber))
|
||||
await _hubSend.SendUserCIdAsync(cid, "OnFishResync", new
|
||||
{
|
||||
roomId,
|
||||
timestamp = now.ToUnixTimeMilliseconds(),
|
||||
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
||||
count = fish.Count,
|
||||
fish
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把场上活鱼转成下发 payload:单体鱼逐条、鱼阵按 FishArrayNumber 分组(鱼阵用首条 fishId 作主键)。
|
||||
/// createdAt / expireAt 一律取服务端原始值,客户端据此按 (当前时间 - createdAt) / 路径总时长 推算位置。
|
||||
/// </summary>
|
||||
private static List<object> BuildActiveFishPayload(List<ActiveFish> activeFishes)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return activeFishes
|
||||
.Where(f => f != null && string.IsNullOrWhiteSpace(f.FishArrayNumber))
|
||||
.Select(f => (object)new
|
||||
{
|
||||
fishId = f.FishId,
|
||||
@@ -87,7 +147,7 @@ namespace Service.Fishing
|
||||
})
|
||||
.Concat(
|
||||
activeFishes
|
||||
.Where(f => !string.IsNullOrWhiteSpace(f.FishArrayNumber))
|
||||
.Where(f => f != null && !string.IsNullOrWhiteSpace(f.FishArrayNumber))
|
||||
.GroupBy(f => f.FishArrayNumber)
|
||||
.Select(group =>
|
||||
{
|
||||
@@ -107,50 +167,6 @@ namespace Service.Fishing
|
||||
};
|
||||
}))
|
||||
.ToList();
|
||||
compatibilityFishPayload = activeFishes
|
||||
.Where(f => string.IsNullOrWhiteSpace(f.FishArrayNumber) && f.FishType == 1)
|
||||
.Select(f => (object)new
|
||||
{
|
||||
fishId = f.FishId,
|
||||
fishIds = new[] { f.FishId },
|
||||
fishNumber = f.FishNumber,
|
||||
routeId = f.ClientRouteId,
|
||||
fishArrayNumber = string.Empty,
|
||||
createdAt = new DateTimeOffset(f.CreatedAt).ToUnixTimeMilliseconds(),
|
||||
createdTime = f.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
||||
expireAt = new DateTimeOffset(f.ExpireAt).ToUnixTimeMilliseconds(),
|
||||
expireTime = f.ExpireAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
||||
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(f.ExpireAt) - now).TotalSeconds)
|
||||
}).ToList();
|
||||
|
||||
payload = new
|
||||
{
|
||||
roomInfo.RoomId,
|
||||
roomInfo.RoomName,
|
||||
roomInfo.GameType,
|
||||
roomInfo.RoomLevel,
|
||||
roomInfo.RoomPartition,
|
||||
roomInfo.RoomNumber,
|
||||
roomInfo.MaxPlayerCount,
|
||||
roomInfo.CurrentOnlinePlayerCount,
|
||||
roomInfo.RobotCount,
|
||||
roomInfo.Players,
|
||||
roomInfo.CreateUserId,
|
||||
roomInfo.SystemFactor,
|
||||
activeFishes = activeFishPayload
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = roomInfo;
|
||||
}
|
||||
await _hubSend.SendUserCIdAsync(cid, "RoomInfo", payload);
|
||||
if (compatibilityFishPayload != null && compatibilityFishPayload.Count > 0)
|
||||
{
|
||||
_ = SendInitialFishAfterSceneReady(cid, roomInfo.RoomId, compatibilityFishPayload);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendInitialFishAfterSceneReady(string cid, string roomId, List<object> initialFishPayload)
|
||||
|
||||
@@ -22,7 +22,12 @@ namespace FishGameService.GameSend
|
||||
|
||||
void RoomJoinNotice(string roomId, RedisGamePlayer player);
|
||||
|
||||
Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null);
|
||||
Task SendRoomInfo(string cid, RedisGameRoom roomInfo, List<ActiveFish> activeFishes = null, bool sendInitialFish = true);
|
||||
|
||||
/// <summary>
|
||||
/// 补发场上活鱼(挂起恢复 / 重连专用独立协议,事件名 OnFishResync,全量替换语义)。
|
||||
/// </summary>
|
||||
Task FishResyncNoticeAsync(string cid, string roomId, List<ActiveFish> activeFishes);
|
||||
|
||||
void RoomLeaveNotice(string roomId, string userId);
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace FishGameService.Hubs
|
||||
await Groups.AddToGroupAsync(cid, room.RoomId);
|
||||
|
||||
var activeFishes = _fishingService.GetActiveFishes(room.RoomId);
|
||||
await _send.SendRoomInfo(cid, room, activeFishes);
|
||||
await _send.SendRoomInfo(cid, room, activeFishes, sendInitialFish: false);
|
||||
_send.RoomJoinNotice(room.RoomId, player);
|
||||
await Clients.Caller.SendAsync("OnPlayerInfo", new
|
||||
{
|
||||
@@ -191,9 +191,8 @@ namespace FishGameService.Hubs
|
||||
|
||||
await _fishingService.SendRoomChange(room);
|
||||
|
||||
await SendActiveFishSync(room.RoomId);
|
||||
|
||||
|
||||
// 重连补发:走独立协议 OnFishResync(全量替换),不再走 OnFishSpawn(type=sync)
|
||||
await _send.FishResyncNoticeAsync(cid, room.RoomId, activeFishes);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -210,16 +209,6 @@ namespace FishGameService.Hubs
|
||||
_logger.LogWarning(ex, "FishHub重连后更新用户状态失败 userId={UserId} roomId={RoomId}", userId, room.RoomId);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SendActiveFishSync(room.RoomId);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "FishHub重连同服务端步活跃鱼失败 userId={UserId} roomId={RoomId}", userId, room.RoomId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -378,12 +367,14 @@ namespace FishGameService.Hubs
|
||||
try
|
||||
{
|
||||
var room = _redisService.GetGameRoom(roomId);
|
||||
var activeFishes = _fishingService.GetActiveFishes(roomId);
|
||||
if (room != null)
|
||||
{
|
||||
var activeFishes = _fishingService.GetActiveFishes(roomId);
|
||||
await _send.SendRoomInfo(cid, room, activeFishes);
|
||||
|
||||
// 关掉 RoomInfo 搭车的延迟 init,补发统一走独立协议
|
||||
await _send.SendRoomInfo(cid, room, activeFishes, sendInitialFish: false);
|
||||
}
|
||||
// 挂起恢复补发:独立协议 OnFishResync(全量替换)
|
||||
await _send.FishResyncNoticeAsync(cid, roomId, activeFishes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -391,40 +382,6 @@ namespace FishGameService.Hubs
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendActiveFishSync(string roomId)
|
||||
{
|
||||
var activeFishes = _fishingService.GetActiveFishes(roomId);
|
||||
if (activeFishes == null || activeFishes.Count == 0)
|
||||
return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var syncFish = activeFishes.Select(f => new
|
||||
{
|
||||
fishId = f.FishId,
|
||||
fishIds = new[] { f.FishId },
|
||||
fishNumber = string.IsNullOrWhiteSpace(f.FishArrayNumber) ? f.FishNumber : string.Empty,
|
||||
routeId = f.ClientRouteId,
|
||||
fishArrayNumber = f.FishArrayNumber ?? string.Empty,
|
||||
createdAt = new DateTimeOffset(f.CreatedAt).ToUnixTimeMilliseconds(),
|
||||
createdTime = f.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
||||
expireAt = new DateTimeOffset(f.ExpireAt).ToUnixTimeMilliseconds(),
|
||||
expireTime = f.ExpireAt.ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
||||
remainingSeconds = (int)Math.Max(0, (new DateTimeOffset(f.ExpireAt) - now).TotalSeconds)
|
||||
}).ToList();
|
||||
|
||||
var payload = new
|
||||
{
|
||||
type = "sync",
|
||||
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
||||
fish = syncFish
|
||||
};
|
||||
|
||||
|
||||
await Clients.Caller.SendAsync("OnFishSpawn", payload);
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<string>> BotFireCannon(string roomId, string botUserId, long gold, int count)
|
||||
{
|
||||
string cid = Context.ConnectionId;
|
||||
|
||||
Reference in New Issue
Block a user