Files
BuYuHouDuan/FishGameSrc/FishGameService/GameService/FishingGameService.cs
T
2026-10-13 21:52:02 +08:00

3145 lines
128 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Numerics;
using System.Text.Json;
using AutoMapper;
using Common.Redis;
using UserDomain.Entity;
using Common.Utils;
using CommonDomain;
using CommonDomain.BaseModel;
using CommonDomain.Enum;
using CommonService;
using DotNetCore.CAP;
using FishGameService.Config;
using FishGameService.GameSend;
using FishGameService.Hubs;
using FishGameService.Services;
using FishGameService.Utils;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using MongoRepository;
using NewLife;
using RedisHelper;
using RedisService;
using RedisService.Param;
using UserDomain.Entity;
using UserDomain.Enum;
using UserService.Param;
using UserService.Tasks.Param;
using UserService.Hubs;
using UserService.User;
using XAct.Users;
namespace FishGameService.GameService
{
public class FishingGameService : IFishingGameService, IPendingSettlementService
{
private readonly ICapPublisher _capPublisher;
private readonly IRedisService _redisService;
private readonly ILogger<FishingGameService> _logger;
private readonly IHubSend<FishGameHub> _hubGame;
private readonly IFishingSendService _sendResult;
private GameConfig _gameConfig;
private ActivityConfig _activityConfig;
private static GameConfig _sharedGameConfig;
private static ActivityConfig _sharedActivityConfig;
private static DateTime _sharedBaseConfigLastFetch = DateTime.MinValue;
private static readonly TimeSpan BaseConfigCacheDuration = TimeSpan.FromMinutes(5);
private static readonly object BaseConfigLock = new();
private static FishingKillConfig _fishingKillConfig;
private static DateTime _fishingKillLastFetch = DateTime.MinValue;
private static readonly TimeSpan FishingKillCacheDuration = TimeSpan.FromMinutes(30);
private static readonly object _fishingKillLock = new();
private readonly IUserService _userService;
private readonly IMapper _mapper;
private readonly IDistributedLock _distributedLock;
private readonly BulletConfig _bulletConfig;
private readonly IMongoBaseRepository<BotProfileEntity> _botProfileRepo;
private readonly IBulletTracker _bulletTracker;
private readonly IFishKillTracker _fishKillTracker;
private readonly IHubContext<LoginHub> _hubLoginContext;
private readonly FishSpawnService _fishSpawnService;
private static readonly ConcurrentDictionary<string, CancellationTokenSource> PendingDisconnects = new();
private static readonly int DisconnectTimeoutSeconds = 60;
private static readonly ConcurrentDictionary<string, (PlayerFactor Factor, DateTime RefreshAt)> PlayerFactorCache = new();
private static readonly TimeSpan FactorRefreshInterval = TimeSpan.FromMinutes(5);
private static long _cachedSystemGold;
private static DateTime _systemGoldLastFetch = DateTime.MinValue;
private static readonly TimeSpan SystemGoldCacheDuration = TimeSpan.FromMinutes(5);
private static readonly object _systemGoldLock = new();
private static readonly ConcurrentDictionary<string, DateTime> _lastFactorPersist = new();
private static readonly TimeSpan FactorPersistInterval = TimeSpan.FromSeconds(30);
public FishingGameService(ICapPublisher capPublisher, IRedisService redisService, IHubSend<FishGameHub> hubGame, IFishingSendService sendResult, IUserService userService, IMapper mapper, IDistributedLock distributedLock, BulletConfig bulletConfig, ILogger<FishingGameService> logger, IMongoBaseRepository<BotProfileEntity> botProfileRepo, IBulletTracker bulletTracker, IFishKillTracker fishKillTracker, IHubContext<LoginHub> hubLoginContext, FishSpawnService fishSpawnService)
{
_capPublisher = capPublisher;
_redisService = redisService;
_hubGame = hubGame;
_sendResult = sendResult;
_userService = userService;
LoadBaseConfigs(redisService, out _gameConfig, out _activityConfig);
_mapper = mapper;
_distributedLock = distributedLock;
_bulletConfig = bulletConfig;
_logger = logger;
_botProfileRepo = botProfileRepo;
_bulletTracker = bulletTracker;
_fishKillTracker = fishKillTracker;
_hubLoginContext = hubLoginContext;
_fishSpawnService = fishSpawnService;
}
private static void LoadBaseConfigs(IRedisService redisService, out GameConfig gameConfig, out ActivityConfig activityConfig)
{
var now = DateTime.UtcNow;
if (_sharedGameConfig == null || _sharedActivityConfig == null
|| now - _sharedBaseConfigLastFetch > BaseConfigCacheDuration)
{
lock (BaseConfigLock)
{
if (_sharedGameConfig == null || _sharedActivityConfig == null
|| now - _sharedBaseConfigLastFetch > BaseConfigCacheDuration)
{
_sharedGameConfig = redisService.GetGameConfig() ?? _sharedGameConfig;
_sharedActivityConfig = redisService.GetActivityConfig() ?? _sharedActivityConfig;
_sharedBaseConfigLastFetch = now;
}
}
}
gameConfig = _sharedGameConfig;
activityConfig = _sharedActivityConfig;
}
public async Task JoinRoom(string cid, string roomId, string userId, int posituin)
{
var existingSession = _redisService.GetRedisUserCId(userId);
if (!string.IsNullOrWhiteSpace(existingSession?.RoomId)
&& existingSession.RoomId != roomId)
{
await SettleGoldAsync(userId);
}
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
room = new RedisGameRoom
{
RoomId = roomId,
RoomName = "新手渔场",
GameType = GameTypeEnum.风雷之渊,
RoomLevel = 1,
RoomPartition = 0,
RoomNumber = 1,
MaxPlayerCount = 4,
CurrentOnlinePlayerCount = 0,
RobotCount = 0,
Players = new List<RedisGamePlayer>(),
CreateUserId = ""
};
_redisService.SetGameRoom(room);
}
NormalizeRoomPlayerCounts(room, preserveEmptyRoomRobotPlan: true);
var existingPlayer = room.Players.FirstOrDefault(c => c.UserId == userId && !c.IsBot);
if (existingPlayer != null)
{
CancelPendingDisconnect(userId);
var oldCId = existingPlayer.CId;
existingPlayer.State = 0;
existingPlayer.CId = cid;
if (string.IsNullOrWhiteSpace(room.CreateUserId)
|| !room.Players.Any(p => !p.IsBot && p.UserId == room.CreateUserId))
room.CreateUserId = existingPlayer.UserId;
if (!string.IsNullOrWhiteSpace(oldCId) && oldCId != cid)
{
if (!string.IsNullOrWhiteSpace(roomId))
{
var oldRoom = _redisService.GetGameRoom(roomId);
if (oldRoom != null)
{
var oldPlayer = oldRoom.Players.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (oldPlayer != null && oldPlayer.CId == oldCId)
{
oldPlayer.CId = "";
oldPlayer.State = 1;
_redisService.SetGameRoom(oldRoom);
}
}
}
}
else
{
}
_hubGame.AddGroup(cid, roomId);
var userRedis1 = _redisService.GetRedisUserCId(userId) ?? new RedisUserCid { UserId = userId };
userRedis1.RoomId = roomId;
userRedis1.CId = cid;
userRedis1.PartitionId = "";
_redisService.SetRedisUserCId(userRedis1);
_redisService.SetUserId(cid, userId);
NormalizeRoomPlayerCounts(room);
_redisService.SetGameRoom(room);
_fishSpawnService.RegisterRoom(roomId, (int)room.GameType, room.RoomLevel);
var fishes = _fishSpawnService.GetActiveFishes(roomId);
await _sendResult.SendRoomInfo(cid, room, fishes);
_hubGame.SendUserCId(cid, "OnPlayerInfo", new
{
userId = existingPlayer.UserId,
userName = existingPlayer.UserName,
gold = existingPlayer.Gold,
diamond = existingPlayer.Diamond,
viplvl = existingPlayer.VipLevel,
cannonMultiple = existingPlayer.CannonMultiple,
cannonId = existingPlayer.CannonId,
wingsId = existingPlayer.WingsId,
position = existingPlayer.Position
});
// _sendResult.RoomJoinNotice(roomId, existingPlayer);
_sendResult.ChangeControllerUser(roomId, room.CreateUserId);
await SendRoomChange(room);
return;
}
if (RoomBotFactory.FixedBotSeatsEnabled && posituin >= 0 && RoomBotFactory.FixedBotSeats.Contains(posituin))
{
posituin = -1;
}
if (room.Players.Any(c => c.Position == posituin))
{
var occ = room.Players.Select(p => p.Position).ToHashSet();
var nextPos = -1;
for (int p = 0; p < room.MaxPlayerCount; p++)
{
if (!occ.Contains(p)
&& !(RoomBotFactory.FixedBotSeatsEnabled && RoomBotFactory.FixedBotSeats.Contains(p)))
{
nextPos = p;
break;
}
}
if (nextPos < 0)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间已满, "房间人数已满");
return;
}
posituin = nextPos;
}
if (room.Players.Count >= room.MaxPlayerCount)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间已满, "房间人数已满");
return;
}
var existingRedis = _redisService.GetRedisUserCId(userId);
if (existingRedis != null && !string.IsNullOrWhiteSpace(existingRedis.RoomId) && existingRedis.RoomId != roomId)
{
await CleanupRoomAsync(userId, force: true);
}
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis == null)
userRedis = new RedisUserCid() { UserId = userId };
userRedis.RoomId = roomId;
userRedis.CId = cid;
userRedis.PartitionId = "";
UserEntity user;
try
{
user = await _userService.GetUser(userId);
}
catch
{
user = null;
}
if (user == null)
{
user = new UserEntity();
user.BaseInfo.UserName = userId;
user.BaseInfo.AvatarUrl = "";
user.Assets.Gold = 100000;
user.Assets.Diamond = 0;
user.Assets.VipLevel = 0;
}
userRedis.UserName = user.BaseInfo.UserName ?? userId;
try
{
user.Static.RoomNumber = roomId;
user.Static.CurrentGameType = (int)room.GameType;
await _userService.UpdateUser(user);
}
catch
{
// ignore update failures
}
if (_gameConfig == null)
{
var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config", "gameconfig.json");
if (File.Exists(configPath))
{
var json = File.ReadAllText(configPath);
_gameConfig = System.Text.Json.JsonSerializer.Deserialize<GameConfig>(json, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
else
{
_gameConfig = new GameConfig
{
RoomConfigs = new List<RoomConfig>
{
new RoomConfig { RoomNumber = "1", RoomName = "新手渔场", GameType = GameTypeEnum.风雷之渊, RoomLevel = 1, RoomPartitionNum = 1, RoomCount = 1, RoomGoldLimit = 0, CannonMultipleList = new List<int> { 10, 20, 50, 100, 200 } }
},
FishRoomConfig = new FishRoomConfig { SeatSelectLimit = new List<int> { 1, 2 } }
};
var dir = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir);
var json = System.Text.Json.JsonSerializer.Serialize(_gameConfig, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(configPath, json);
}
_redisService.SetGameConfig(_gameConfig);
}
var roomConfig = _gameConfig.RoomConfigs?.Where(c => c.GameType == room.GameType && c.RoomLevel == room.RoomLevel).FirstOrDefault();
if (roomConfig == null)
{
roomConfig = new RoomConfig
{
RoomNumber = "1",
RoomName = "新手渔场",
GameType = GameTypeEnum.风雷之渊,
RoomLevel = 1,
RoomPartitionNum = 1,
RoomCount = 1,
RoomGoldLimit = 0,
CannonMultipleList = new List<int> { 10, 20, 50, 100, 200 }
};
}
if (user.Assets.Gold < roomConfig.RoomGoldLimit)
{
_sendResult.ErrorMessage(cid, ErrorCodes.金币不足, "金币不足 不满足房间要求");
return;
}
var occupied = room.Players.Select(p => p.Position).ToHashSet();
int naturalPos = -1;
for (int p = 0; p < room.MaxPlayerCount; p++)
{
if (!occupied.Contains(p)
&& !(RoomBotFactory.FixedBotSeatsEnabled && RoomBotFactory.FixedBotSeats.Contains(p)))
{
naturalPos = p;
break;
}
}
if (naturalPos < 0)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间已满, "房间人数已满");
return;
}
if (posituin < 0)
{
posituin = naturalPos;
}
else if (posituin != naturalPos)
{
var seatLimit = _gameConfig?.FishRoomConfig?.SeatSelectLimit ?? new List<int>();
bool canManualSelect = false;
if (seatLimit.Any(c => c == 3))
canManualSelect = true;
if (seatLimit.Any(c => c == 1) && user.Assets.VipLevel > 0)
{
var vipLevels = _gameConfig?.FishRoomConfig?.SeatSelectVipLevels ?? new List<int>();
if (vipLevels.Count == 0 || vipLevels.Contains(user.Assets.VipLevel))
canManualSelect = true;
}
if (seatLimit.Any(c => c == 2) && user.UserType == 2)
canManualSelect = true;
if (!canManualSelect)
{
var roles = new List<string>();
if (seatLimit.Any(c => c == 1)) roles.Add("VIP");
if (seatLimit.Any(c => c == 2)) roles.Add("代理");
var msg = roles.Count > 0 ? $"只有{string.Join("/", roles)}才能选择座位" : "没有权限选择座位";
_sendResult.ErrorMessage(cid, ErrorCodes.未知错误, msg);
return;
}
}
_redisService.SetUserId(cid, userId);
_redisService.SetRedisUserCId(userRedis);
//var systemTotal= _redisService.GetTotalRechargeAmount();
//var systemYue = _redisService.GetTotalUserGold();
//(systemTotal - systemYue)
var player = new RedisGamePlayer();
player.UserId = userId;
player.UserName = user.BaseInfo.UserName;
player.UserNumber = user.BaseInfo.Number;
player.Avatar = user.BaseInfo.AvatarUrl;
player.IsRedName = user.BaseInfo.IsRedName;
player.Level = user.Assets.Level;
player.Exp = user.Assets.Exp;
var levelCfg = _gameConfig?.UserLevelConfigs?.FirstOrDefault(c => c.Level == user.Assets.Level);
player.MaxExp = levelCfg?.ExpMax ?? 0;
player.VipLevel = user.Assets.VipLevel;
player.Gold = user.Assets.Gold;
player.Diamond = user.Assets.Diamond;
player.CannonId = user.CannonUsing?.Number;
player.WingsId = user.WingUsing?.Number;
player.WarGodId = user.GodWarUsing?.Number;
player.CannonMultiple = roomConfig.CannonMultipleList.FirstOrDefault();
player.BeginCoin = player.Gold;
player.MaxGold = player.Gold;
player.Position = posituin;
player.CId = cid;
player.BeginTime = DateTime.Now.ToTimeStamp();
player.PlayerFactor = user.PlayerFactor;
player.TotalRechargeGold = user.Static.TotalRechargeGold;
player.SafeBoxGold = user.SafeBox?.Amount ?? 0;
room.Players.Add(player);
if (string.IsNullOrWhiteSpace(room.CreateUserId)
|| !room.Players.Any(p => !p.IsBot && p.UserId == room.CreateUserId))
room.CreateUserId = player.UserId;
room.CurrentOnlinePlayerCount++;
_hubGame.AddGroup(cid, roomId);
_sendResult.ChangeControllerUser(roomId, room.CreateUserId);
var latestGameConfig = _redisService.GetGameConfig() ?? _gameConfig;
_gameConfig = latestGameConfig;
var botsEnabled = latestGameConfig?.BotConfig?.Enabled == true;
if (!botsEnabled)
{
var existingBots = room.Players.Where(p => p.IsBot).ToList();
foreach (var bot in existingBots)
await RemoveBotFromRoom(room, bot, sendLeaveRoom: true, showWaiting: false);
room.RobotCount = 0;
_redisService.SetGameRoom(room);
}
int diagBotCount = room.Players.Count(p => p.IsBot);
bool diagIsFirstHuman = room.Players.Count(p => !p.IsBot && p.State == 0) == 1;
if (botsEnabled && diagIsFirstHuman)
{
int currentBotCount = room.Players.Count(p => p.IsBot);
if (currentBotCount < room.RobotCount)
{
CreateRoomBots(room, roomConfig);
}
else
{
}
}
NormalizeRoomPlayerCounts(room);
room.Players = room.Players.OrderBy(c => c.Position).ToList();
_redisService.SetGameRoom(room);
_fishSpawnService.RegisterRoom(roomId, (int)room.GameType, room.RoomLevel);
List<ActiveFish> activeFishes = _fishSpawnService.GetActiveFishes(roomId);
await _sendResult.SendRoomInfo(cid, room, activeFishes);
_sendResult.RoomJoinNotice(roomId, player);
_hubGame.SendGroup(roomId, "OnSeatChange", new
{
type = "join",
playerId = player.Position + 1,
position = player.Position,
playerName = player.UserName,
nickname = player.UserName,
gold = player.Gold,
diamond = player.Diamond,
vipLevel = player.VipLevel,
turretId = 1,
turretRate = 1,
turretLv = 1,
headId = 1,
roomRate = 1,
bulletPower = 1
});
_hubGame.SendUserCId(cid, "OnPlayerInfo", new
{
userId = player.UserId,
userName = player.UserName,
gold = player.Gold,
diamond = player.Diamond,
viplvl = player.VipLevel,
cannonMultiple = player.CannonMultiple,
cannonId = player.CannonId,
wingsId = player.WingsId,
position = player.Position
});
_redisService.SetGameRoom(room);
await SendRoomChange(room);
}
public async Task LeaveRoom(string cid, string roomId, string userId)
{
var leaveRequestTimestamp = Stopwatch.GetTimestamp();
if (string.IsNullOrWhiteSpace(userId))
userId = _redisService.GetUserId(cid);
var markedLeaving = await _distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
() => MarkPlayerLeavingAsync(cid, roomId, userId),
retry: true);
if (!markedLeaving)
return;
await _distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
() => CompleteLeaveIfPendingAsync(cid, roomId, userId, leaveRequestTimestamp),
retry: true);
}
private async Task CompleteLeaveIfPendingAsync(string cid, string roomId, string userId, long leaveRequestTimestamp)
{
var room = _redisService.GetGameRoom(roomId);
var player = room?.Players?.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (player != null && player.State != 2)
{
return;
}
await LeaveRoomValidated(cid, roomId, userId, leaveRequestTimestamp);
}
private Task<bool> MarkPlayerLeavingAsync(string cid, string roomId, string userId)
{
if (!IsCallerUser(cid, roomId, userId))
{
return Task.FromResult(false);
}
var room = _redisService.GetGameRoom(roomId);
var player = room?.Players?.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (room == null || player == null)
return Task.FromResult(true);
player.State = 2;
_redisService.SetGameRoom(room);
return Task.FromResult(true);
}
private async Task LeaveRoomValidated(string cid, string roomId, string userId, long leaveRequestTimestamp)
{
if (string.IsNullOrWhiteSpace(userId))
{
userId = _redisService.GetUserId(cid);
}
CancelPendingDisconnect(userId);
if (!IsCallerUser(cid, roomId, userId))
{
var mappedUserId = string.IsNullOrWhiteSpace(cid) ? null : _redisService.GetUserId(cid);
var session = _redisService.GetRedisUserCId(userId);
if (!string.IsNullOrWhiteSpace(cid))
_hubGame.SendUserCId(cid, "KickToMainPage", new { userId });
return;
}
await LeaveRoomInternal(cid, roomId, userId, leaveRequestTimestamp);
}
private async Task LeaveRoomInternal(string cid, string roomId, string userId, long leaveRequestTimestamp = 0)
{
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return;
}
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis != null)
{
if (userRedis.RoomId != roomId)
{
}
}
var playerInfo = room.Players.Where(p => p.UserId == userId).FirstOrDefault();
if (playerInfo == null)
{
await TryFlushFishingProgress(roomId, userId, null);
_redisService.ClearPlayerRuntimeData(roomId, userId);
PlayerFactorCache.TryRemove($"{roomId}|{userId}", out _);
_bulletTracker.Clear(roomId, userId);
if (!string.IsNullOrWhiteSpace(cid))
{
_sendResult.RoomLeaveNotice(roomId, userId);
_hubGame.RemoveGroup(cid, roomId);
}
if (userRedis?.RoomId == roomId)
{
userRedis.RoomId = "";
userRedis.CId = "";
userRedis.PartitionId = "";
_redisService.SetRedisUserCId(userRedis);
}
try
{
var user = await _userService.GetUser(userId);
if (user != null)
{
user.Static ??= new StaticInfo();
user.Static.RoomNumber = string.Empty;
user.Static.CurrentGameType = 0;
await _userService.UpdateUser(user);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "LeaveRoom幂等分支清除房间状态失败 roomId={RoomId} userId={UserId}", roomId, userId);
}
return;
}
try
{
var user = await _userService.GetUser(userId);
if (user != null)
{
user.Static ??= new StaticInfo();
user.Static.RoomNumber = string.Empty;
user.Static.CurrentGameType = 0;
if (playerInfo.BeginTime > 0)
{
var duration = DateTime.Now.ToTimeStamp() - playerInfo.BeginTime;
if (duration > 0)
{
user.Static.OnlineTime += duration;
user.Static.GameTime += duration;
}
}
if (playerInfo.PlayerFactor != null)
{
if (playerInfo.PlayerFactor.CompensateTargetGold > 0)
{
var remainingQuota = Math.Max(
0L,
playerInfo.PlayerFactor.CompensateTargetGold - playerInfo.Gold);
playerInfo.PlayerFactor.CompensateQuota = (int)Math.Min(int.MaxValue, remainingQuota);
if (remainingQuota <= 0)
{
playerInfo.PlayerFactor.CompensateTargetGold = 0;
playerInfo.PlayerFactor.CompensateStartGold = 0;
}
}
if (user.PlayerFactor != null)
{
user.PlayerFactor.CompensateQuota = playerInfo.PlayerFactor.CompensateQuota;
user.PlayerFactor.CompensateTargetGold = playerInfo.PlayerFactor.CompensateTargetGold;
user.PlayerFactor.CompensateStartGold = playerInfo.PlayerFactor.CompensateStartGold;
user.PlayerFactor.PenaltyQuota = playerInfo.PlayerFactor.PenaltyQuota;
}
}
await _userService.UpdateUser(user);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "LeaveRoom更新在线时长失败 roomId={RoomId} userId={UserId}", roomId, userId);
}
await TryFlushFishingSettlement(room, playerInfo);
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.SyncPlayerGold(cid, playerInfo.Gold);
_redisService.ClearPlayerRuntimeData(roomId, userId);
PlayerFactorCache.TryRemove($"{roomId}|{userId}", out _);
_bulletTracker.Clear(roomId, userId);
room.Players.Remove(playerInfo);
if (room.CreateUserId == playerInfo.UserId && room.Players.Count() > 0)
{
var onlinePlayer = room.Players.FirstOrDefault(p => !p.IsBot && p.State == 0);
var realPlayer = onlinePlayer ?? room.Players.FirstOrDefault(p => !p.IsBot);
room.CreateUserId = realPlayer?.UserId ?? string.Empty;
if (!string.IsNullOrWhiteSpace(room.CreateUserId))
_sendResult.ChangeControllerUser(roomId, room.CreateUserId);
}
NormalizeRoomPlayerCounts(room);
bool hasNoRealPlayers = !room.Players.Any(p => !p.IsBot);
if (hasNoRealPlayers)
{
var keptRobotCount = room.RobotCount;
room.Players.Clear();
room.CreateUserId = string.Empty;
room.CurrentOnlinePlayerCount = 0;
room.RobotCount = Math.Clamp(keptRobotCount, 0, Math.Max(0, room.MaxPlayerCount));
ClearKillFishIds(roomId);
}
_redisService.SetGameRoom(room);
if (hasNoRealPlayers)
_fishSpawnService.UnregisterRoom(roomId);
_hubGame.SendGroup(room.RoomId, "OnSeatChange", new
{
type = "leave",
userId = userId,
playerId = playerInfo.Position + 1,
position = playerInfo.Position,
showWaiting = false
});
if (userRedis?.RoomId == roomId)
{
userRedis.RoomId = "";
userRedis.CId = "";
userRedis.PartitionId = "";
_redisService.SetRedisUserCId(userRedis);
}
if (!string.IsNullOrWhiteSpace(cid))
{
_sendResult.RoomLeaveNotice(roomId, userId);
var leaveElapsedMs = leaveRequestTimestamp == 0
? -1D
: Stopwatch.GetElapsedTime(leaveRequestTimestamp).TotalMilliseconds;
_hubGame.RemoveGroup(cid, roomId);
}
else
{
_hubGame.SendGroup(roomId, "RoomLeaveNotice", userId);
}
await SendRoomChange(room);
}
public async Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFish(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
{
return await _distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
() => KillFishInternal(cid, roomId, userId, fishNumber, fihsId, gold, timespan, bulletId, timestamp, sign),
retry: true);
}
private async Task<(long gold, long gainExp, long exp, int level, long expMax)> KillFishInternal(string cid, string roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign)
{
if (!VerifySign(cid, $"{roomId}|{userId}|{fishNumber}|{fihsId}|{gold}|{timespan}|{bulletId}|{timestamp}", timestamp, sign))
{
_sendResult.ErrorMessage(cid, ErrorCodes.签名错误, "签名错误");
return (0, 0, 0, 0, 0);
}
;
if (string.IsNullOrWhiteSpace(fishNumber) &&
long.TryParse(fihsId, out var syncedFishId) &&
_fishSpawnService.TryGetFishNumber(roomId, syncedFishId, out var resolvedFishNumber))
{
fishNumber = resolvedFishNumber;
}
var killConfig = GetFishingKillConfig();
var fishConfig = killConfig?.FishTypeConfigs?.FirstOrDefault(c => c.FishNumber == fishNumber);
if (fishConfig == null)
{
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.鱼配置不存在, "鱼配置不存在");
return (0, 0, 0, 0, 0);
}
var gameRoom = _redisService.GetGameRoom(roomId);
if (gameRoom == null)
{
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间不存在");
return (0, 0, 0, 0, 0);
}
var player = gameRoom.Players.Where(c => c.UserId == userId).FirstOrDefault();
if (player == null)
{
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.玩家不在房间, "玩家不在房间");
return (0, 0, 0, 0, 0);
}
if (player.State == 2)
{
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
if (!IsCallerPlayer(cid, gameRoom, player))
{
_hubGame.SendUserCId(cid, "KickToMainPage", new { userId });
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
if (gold <= 0 || string.IsNullOrWhiteSpace(bulletId))
{
_sendResult.ErrorMessage(cid, ErrorCodes.子弹参数无效, "子弹参数无效");
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
if (!TryConsumeBullet(roomId, userId, bulletId))
{
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
var isKill = false;
long killExpGain = 0;
var factor = 0.0;
var playerFactor = await GetPlayerFactorAsync(roomId, userId);
long? serverFishId = null;
if (long.TryParse(fihsId, out var fishIdLong))
{
serverFishId = fishIdLong;
if (!_fishSpawnService.ValidateFishHit(roomId, fishIdLong))
{
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
}
bool isMiss = false;
if (playerFactor.MissRate > 0)
{
isMiss = Random((int)(playerFactor.MissRate * 100), 10000);
}
double killWeight = fishConfig.KillWeight;
int randomRoll = 0;
if (!isMiss)
{
long totalGold = player.Gold + player.SafeBoxGold;
long actualProfit = totalGold - (player.TotalRechargeGold + playerFactor.StartingBonus);
double rawPlayer = -actualProfit / (_gameConfig.KillConfig.NormalizationFactorPlayer * 1.0);
double playerModifier = 1.0 + rawPlayer;
double clampedPlayer = Math.Clamp(playerModifier, _gameConfig.KillConfig.MinPlayerModifier, _gameConfig.KillConfig.MaxPlayerModifier);
string quotaInfo;
double fishRate = 1.0;
double userfactor;
if (playerFactor.IsBlack)
{
userfactor = _gameConfig.KillConfig.ViolateValue;
quotaInfo = $"黑名单 系统违规值={_gameConfig.KillConfig.ViolateValue}";
}
else
{
if (playerFactor.CompensateQuota > 0
&& (playerFactor.CompensateTargetGold <= 0 || player.Gold < playerFactor.CompensateTargetGold))
{
fishRate = fishConfig.FishType switch
{
FishTypeEnum.普通鱼 => playerFactor.CompensateRate.Ordinary,
FishTypeEnum.Boss鱼 or FishTypeEnum.特殊Boss => playerFactor.CompensateRate.BossRate,
FishTypeEnum.黄金鱼 => playerFactor.CompensateRate.GoldRate,
FishTypeEnum.特殊鱼 => playerFactor.CompensateRate.SpecialRate,
_ => 1.0
};
quotaInfo = $"补偿 Quota={playerFactor.CompensateQuota} fishRate={fishRate}";
}
else if (playerFactor.PenaltyQuota > 0)
{
fishRate = fishConfig.FishType switch
{
FishTypeEnum.普通鱼 => playerFactor.PenaltyRate.Ordinary,
FishTypeEnum.Boss鱼 or FishTypeEnum.特殊Boss => playerFactor.PenaltyRate.BossRate,
FishTypeEnum.黄金鱼 => playerFactor.PenaltyRate.GoldRate,
FishTypeEnum.特殊鱼 => playerFactor.PenaltyRate.SpecialRate,
_ => 1.0
};
quotaInfo = $"罚款 Quota={playerFactor.PenaltyQuota} fishRate={fishRate}";
}
else
{
quotaInfo = "无配额";
}
double baseFactor = Math.Round(Math.Clamp(clampedPlayer, _gameConfig.KillConfig.MinPlayerModifier, _gameConfig.KillConfig.MaxPlayerModifier), 2);
if (Math.Abs(baseFactor - playerFactor.ViolateValue) >= 0.01)
{
playerFactor.ViolateValue = baseFactor;
player.PlayerFactor = playerFactor;
_redisService.SetPlayerFactor(roomId, userId, playerFactor);
_redisService.SetGameRoom(gameRoom);
var lastSave = _lastFactorPersist.GetOrAdd(userId, DateTime.MinValue);
if (DateTime.UtcNow - lastSave > FactorPersistInterval)
{
try
{
var user = await _userService.GetUser(userId);
if (user?.PlayerFactor != null)
{
user.PlayerFactor.ViolateValue = baseFactor;
await _userService.UpdateUser(user);
_lastFactorPersist[userId] = DateTime.UtcNow;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "持久化平滑因子失败 userId={UserId} factor={Factor}", userId, baseFactor);
}
}
}
userfactor = playerFactor.ViolateValue * fishRate *(playerFactor.AdditionalFactor==0?1: playerFactor.AdditionalFactor);
}
var systemGold = GetSystemGoldDayCached();
double systemFactor = 1.0 + (systemGold - _gameConfig.KillConfig.DaySystemGold) / (_gameConfig.KillConfig.NormalizationFactorSystem * 1.0);
systemFactor = Math.Clamp(systemFactor, _gameConfig.KillConfig.MinSystemModifier, _gameConfig.KillConfig.MaxSystemModifier);
factor = userfactor * systemFactor;
SyncSystemFactorToRoom(gameRoom, systemFactor);
double finalKillWeight = killWeight * factor;
randomRoll = System.Random.Shared.Next(10000);
isKill = randomRoll < finalKillWeight;
}
long skillGold;
try
{
skillGold = checked(fishConfig.FishScore * gold);
}
catch (OverflowException)
{
_sendResult.ErrorMessage(cid, ErrorCodes.金币参数超出范围, "金币参数超出范围");
return (player.Gold, 0, player.Exp, player.Level, player.MaxExp);
}
(long gold, double beishu, object res) res;
if (isKill)
res = CalcSkillGold(killConfig.BossConfigs, fishNumber, (int)fishConfig.FishScore, skillGold, roomId, fihsId);
else
res = (skillGold, 0.0, (object)null);
if (isKill)
{
bool finalKill = true;
if (fishNumber == "12030")
{
finalKill = res.res?.GetType().GetProperty("type")?.GetValue(res.res) is int t && t == 2;
}
else if (fishNumber == "12029" || fishNumber == "12034")
{
finalKill = res.res?.GetType().GetProperty("isKill")?.GetValue(res.res) is bool k && k;
}
if (finalKill)
{
var killClaimed = serverFishId.HasValue
? _fishSpawnService.TryMarkFishKilled(roomId, serverFishId.Value)
: fishConfig.FishType == FishTypeEnum.普通鱼
|| _fishKillTracker.TryAdd(roomId, fihsId);
if (!killClaimed)
isKill = false;
}
}
if (isKill)
{
skillGold= res.gold;
player.Gold += skillGold;
if (player.Gold > player.MaxGold) player.MaxGold = player.Gold;
await TryCloseCompensateOnTargetAsync(gameRoom, player, playerFactor, roomId, userId);
long netGold = skillGold - gold;
if (netGold > 0)
{
if (playerFactor.CompensateQuota <= 0 && playerFactor.PenaltyQuota > 0)
{
var consumedQuota = Math.Min((long)playerFactor.PenaltyQuota, netGold);
playerFactor.PenaltyQuota -= (int)consumedQuota;
}
}
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan,res.beishu, gold,fishConfig.FishScore, res.res);
await FishKillControl(cid, gameRoom, player, fishNumber, skillGold, fihsId,gold,timespan, killConfig);
var expGain = fishConfig.FishType is FishTypeEnum.Boss鱼 or FishTypeEnum.特殊鱼 or FishTypeEnum.特殊Boss
? 1000 : fishConfig.FishScore;
if (expGain > 0)
{
var expSaved = false;
try
{
var expResult = await _userService.UpdateAssets(new UserAssetParam
{
UserId = player.UserId,
Exp = expGain,
ChangeType = UserAssetChangeTypeEnum.增加,
Source = RewardSource.击杀鱼,
Remark = "捕鱼经验",
RoomInfo = roomId,
IsAddRecord = false
});
expSaved = expResult.isok;
}
catch (Exception ex)
{
_logger.LogError(ex, "击杀经验实时入库异常 roomId={RoomId} userId={UserId} exp={Exp}", roomId, player.UserId, expGain);
}
if (expSaved)
{
ApplyFishingExp(player, expGain);
killExpGain = expGain;
}
else
{
_logger.LogWarning("击杀经验实时入库未成功,转为 Redis 累计待离房结算 roomId={RoomId} userId={UserId} exp={Exp}", roomId, player.UserId, expGain);
_redisService.AddFishingExpProgress(roomId, player.UserId, expGain);
}
}
if ((fishConfig.FishType == FishTypeEnum.Boss鱼 || fishConfig.FishType == FishTypeEnum.特殊Boss || fishConfig.FishType == FishTypeEnum.特殊鱼)
&& fishNumber != "12028"
&& killConfig.BossDropConfig != null
&& killConfig.BossDropConfig.DropItems != null
&& killConfig.BossDropConfig.DropItems.Count > 0)
{
var dropConfig = killConfig.BossDropConfig;
var totalWeight = dropConfig.NoDropWeight + dropConfig.DropItems.Sum(d => d.Weight);
var roll = System.Random.Shared.Next(totalWeight);
var accumulated = 0;
BossDropItemConfig? wonItem = null;
foreach (var item in dropConfig.DropItems)
{
accumulated += item.Weight;
if (roll < accumulated)
{
wonItem = item;
break;
}
}
if (wonItem != null)
{
await _userService.UpdateAssets(new UserAssetParam
{
UserId = player.UserId,
ChangeType = UserAssetChangeTypeEnum.增加,
RewardItems = new List<BaseProductCount>
{
new BaseProductCount { Number = wonItem.PropNumber, Name = wonItem.PropName, Count = wonItem.Count }
},
Remark = $"Boss掉落-{wonItem.PropName}",
Source = RewardSource.击杀Boss,
IsAddRecord = false
});
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan, res.beishu, gold, fishConfig.FishScore, drop: new { wonItem.PropNumber, wonItem.PropName, wonItem.Count });
}
}
if (fishNumber == "12028" && res.res != null)
{
var scorpionItems = res.res.GetType().GetProperty("items")?.GetValue(res.res) as List<BossConfig.ScorpionItemConfig>;
if (scorpionItems != null)
{
foreach (var item in scorpionItems)
{
var propConfig = _gameConfig.PropConfig.FirstOrDefault(p => p.Number == item.PropNumber);
var propName = propConfig?.Name ?? item.PropNumber;
await _userService.UpdateAssets(new UserAssetParam
{
UserId = player.UserId,
ChangeType = UserAssetChangeTypeEnum.增加,
RewardItems = new List<BaseProductCount>
{
new BaseProductCount { Number = item.PropNumber, Name = propName, Count = item.Count }
},
Remark = $"机械蝎掉落-{propName}",
Source = RewardSource.击杀Boss,
IsAddRecord = false
});
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan, res.beishu, gold, fishConfig.FishScore, drop: new { item.PropNumber, propName, item.Count });
}
}
}
}
if (isKill)
{
_redisService.SetGameRoom(gameRoom);
}
_redisService.AddSystemGoldDay(gold - (isKill ? skillGold : 0));
return (player.Gold, killExpGain, player.Exp, player.Level, player.MaxExp);
}
/// <summary>
/// 击杀经验实时入库成功后,同步房间内玩家的经验/等级/经验上限(与 UserEntity.LevelVerify 的升级判定保持一致)。
/// </summary>
private void ApplyFishingExp(RedisGamePlayer player, long gainExp)
{
player.Exp += gainExp;
var levelCfg = _gameConfig?.UserLevelConfigs?.FirstOrDefault(c => player.Exp >= c.ExpMin && player.Exp < c.ExpMax);
if (levelCfg != null)
{
player.Level = levelCfg.Level;
player.MaxExp = levelCfg.ExpMax;
}
}
private void AddBullets(string roomId, string userId, List<string> bulletIds)
{
_bulletTracker.Add(roomId, userId, bulletIds);
}
private bool TryConsumeBullet(string roomId, string userId, string bulletId)
{
return _bulletTracker.TryConsume(roomId, userId, bulletId);
}
private void ClearBullets(string roomId, string userId)
{
_bulletTracker.Clear(roomId, userId);
}
private bool TryAddKillFishId(string roomId, string fishId)
{
return _fishKillTracker.TryAdd(roomId, fishId);
}
private void ClearKillFishIds(string roomId)
{
_fishKillTracker.Clear(roomId);
}
private async Task<PlayerFactor> GetPlayerFactorAsync(string roomId, string userId)
{
var cacheKey = $"{roomId}|{userId}";
if (PlayerFactorCache.TryGetValue(cacheKey, out var entry)
&& DateTime.UtcNow - entry.RefreshAt < FactorRefreshInterval)
return entry.Factor;
try
{
var user = await _userService.GetUser(userId);
if (user != null && user.PlayerFactor != null)
{
PlayerFactorCache[cacheKey] = (user.PlayerFactor, DateTime.UtcNow);
var room = _redisService.GetGameRoom(roomId);
var player = room?.Players?.FirstOrDefault(p => p.UserId == userId);
if (room != null && player != null)
{
player.PlayerFactor = user.PlayerFactor;
player.SafeBoxGold = user.SafeBox?.Amount ?? 0;
_redisService.SetPlayerFactor(roomId, userId, user.PlayerFactor);
_redisService.SetGameRoom(room);
}
return user.PlayerFactor;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "读取玩家因子失败,降级使用缓存值 roomId={RoomId} userId={UserId}", roomId, userId);
}
if (PlayerFactorCache.TryGetValue(cacheKey, out var stale))
return stale.Factor;
return new PlayerFactor();
}
public static int ClearPlayerFactorCache(string userId)
{
var suffix = $"|{userId}";
var keysToRemove = PlayerFactorCache.Keys.Where(k => k.EndsWith(suffix)).ToList();
var removedCount = 0;
foreach (var key in keysToRemove)
{
if (PlayerFactorCache.TryRemove(key, out _))
removedCount++;
}
return removedCount;
}
public static void ClearAllConfigCache()
{
_sharedBaseConfigLastFetch = DateTime.MinValue;
_fishingKillLastFetch = DateTime.MinValue;
_cachedSystemGold = 0;
_systemGoldLastFetch = DateTime.MinValue;
}
private long GetSystemGoldDayCached()
{
var now = DateTime.UtcNow;
if (_cachedSystemGold == 0 || now - _systemGoldLastFetch > SystemGoldCacheDuration)
{
lock (_systemGoldLock)
{
if (_cachedSystemGold == 0 || now - _systemGoldLastFetch > SystemGoldCacheDuration)
{
_cachedSystemGold = _redisService.GetSystemGoldDay();
_systemGoldLastFetch = now;
}
}
}
return _cachedSystemGold;
}
private FishingKillConfig GetFishingKillConfig()
{
var now = DateTime.UtcNow;
if (_fishingKillConfig == null || now - _fishingKillLastFetch > FishingKillCacheDuration)
{
lock (_fishingKillLock)
{
if (_fishingKillConfig == null || now - _fishingKillLastFetch > FishingKillCacheDuration)
{
var config = _redisService.GetFishingKillConfig();
if (config != null)
{
_fishingKillConfig = config;
_fishingKillLastFetch = now;
}
else if (_gameConfig != null)
{
_fishingKillConfig = new FishingKillConfig
{
FishTypeConfigs = _gameConfig.FishTypeConfigs?.Select(f => new FishTypeBriefConfig
{
FishNumber = f.FishNumber,
FishName = f.FishName,
FishType = f.FishType,
FishScore = f.FishScore,
KillWeight = f.KillWeight
}).ToList(),
KillConfig = _gameConfig.KillConfig,
BossConfigs = _gameConfig.BossConfigs,
BossDropConfig = _gameConfig.BossDropConfig,
BattleOrderIsOpen = _activityConfig?.BattleOrderConfig?.IsOpen ?? false
};
_fishingKillLastFetch = now;
}
}
}
}
return _fishingKillConfig ?? new FishingKillConfig
{
FishTypeConfigs = new(),
KillConfig = new(),
BossConfigs = new(),
BossDropConfig = new()
};
}
public async Task<(List<string> bullets, long gold)> FireCannon(string cid, string roomId, string userId, long gold, int count, long timestamp, string sign, object extra = null)
{
return await _distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
() => FireCannonInternal(cid, roomId, userId, gold, count, timestamp, sign, extra),
retry: true);
}
private async Task<(List<string> bullets, long gold)> FireCannonInternal(string cid, string roomId, string userId, long gold, int count, long timestamp, string sign, object extra)
{
var result = new List<string>();
if (gold <= 0 || count <= 0 || count > 100)
return (result, 0);
if (!VerifySign(cid, $"{roomId}|{userId}|{gold}|{count}|{timestamp}", timestamp, sign))
return (result, 0);
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return (result, 0);
}
var player = room.Players?.FirstOrDefault(c => c.UserId == userId);
if (player == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.玩家不在房间, "玩家不在房间中");
return (result, 0);
}
if (player.State == 2)
{
return (result, player.Gold);
}
if (!IsCallerPlayer(cid, room, player))
{
var mappedUserId = _redisService.GetUserId(cid);
var session = _redisService.GetRedisUserCId(userId);
_hubGame.SendUserCId(cid, "KickToMainPage", new { userId });
return (result, player.Gold);
}
player.CannonMultiple = gold > int.MaxValue ? int.MaxValue : (int)gold;
long totalCost;
try
{
totalCost = checked(gold * count);
}
catch (OverflowException)
{
return (result, player.Gold);
}
if (player.Gold < totalCost)
{
_sendResult.ErrorMessage(cid, ErrorCodes.金币不足, "金币不足");
return (result, player.Gold);
}
var goldBeforeFire = player.Gold;
player.Gold -= totalCost;
if (player.Gold < 0)
player.Gold = 0;
for (int i = 0; i < count; i++)
{
result.Add(Guid.NewGuid().ToString("N"));
}
AddBullets(roomId, userId, result);
_redisService.SetGameRoom(room);
if (player.Gold <= 0)
{
_sendResult.SyncPlayerGold(cid, player.Gold);
}
_sendResult.RoomFireNotice(roomId, userId, result, gold, count, extra);
_redisService.AddFishingConsumeProgress(roomId, userId, totalCost);
if (GetFishingKillConfig().BattleOrderIsOpen)
{
RecordFishingTaskProgress(roomId, userId, TaskEvent.开炮任务, count);
}
return (result, player.Gold);
}
public async Task<int> UseProp(string cid, string roomId, string userId, string propNumber, int diamond = 0)
{
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return -1;
}
if (diamond < 0 || !IsCallerUser(cid, roomId, userId))
{
_sendResult.ErrorMessage(cid, ErrorCodes.道具数量不足, "背包道具数量不足");
return -1;
}
var skillConfig = _gameConfig.PropConfig.FirstOrDefault(c => c.Type == ProductTypeEnum.技能 && c.Number == propNumber);
if (skillConfig == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.道具配置不存在, "道具配置不存在");
return -1;
}
if (diamond > 0)
{
var player = room.Players.Where(c => c.UserId == userId).FirstOrDefault();
if (player == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.用户不存在, "用户不存在");
return -1;
}
if (player.Diamond < diamond)
{
_sendResult.ErrorMessage(cid, ErrorCodes.钻石不足, "玩家钻石不足");
return -1;
}
player.Diamond -= diamond;
_redisService.SetGameRoom(room);
_sendResult.UsePropNotice(roomId, userId, propNumber);
_hubGame.SendUserUserId(userId, "AssetUpdated", new
{
CurrencyType = 2,
ChangeType = 2,
Amount = diamond,
NewGold = player.Gold,
NewDiamond = player.Diamond,
Source = RewardSource.道具.ToString(),
Remark = ""
});
_capPublisher.Publish(TopicKeys.用户资产变更, new UserAssetParam()
{
ChangeType = UserAssetChangeTypeEnum.减少,
UserId = userId,
Remark = "",
Source = RewardSource.道具,
OtherId = "",
RoomName = room.RoomName,
RoomInfo = room.RoomId,
Diamond = diamond,
SyncToFrontend = false
});
}
else
{
_sendResult.UsePropNotice(roomId, userId, propNumber);
var baseProductCount = _mapper.Map<BaseProductCount>(skillConfig);
baseProductCount.Count = 1;
_capPublisher.Publish(TopicKeys.用户资产变更, new UserAssetParam()
{
ChangeType = UserAssetChangeTypeEnum.减少,
UserId = userId,
Gold = 0,
Remark = "",
Source = RewardSource.道具,
OtherId = "",
RoomName = room.RoomName,
RoomInfo = room.RoomId,
RewardItems = new List<BaseProductCount>() { baseProductCount }
});
}
if (skillConfig.Number == "40004" && _activityConfig.BattleOrderConfig.IsOpen)
{
RecordFishingTaskProgress(roomId, userId, TaskEvent.使用狂暴, 1);
}
if (propNumber == "40003")
{
_fishSpawnService.ExtendFishLifespan(roomId, 15);
_hubGame.SendGroup(roomId, "FishFrozen", null);
}
try
{
var user = await _userService.GetUser(userId);
if (user != null)
{
var prop = user.Props.FirstOrDefault(p => p.Number == propNumber);
if (prop != null && prop.Count > 0)
return prop.Count;
return 0;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "UseProp 查询道具剩余数量失败 userId={UserId} propNumber={PropNumber}", userId, propNumber);
}
return -1;
}
public async Task<long> UseTorpedo(string cid, string roomId, string userId, string propNumber)
{
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return -1;
}
if (!IsCallerUser(cid, roomId, userId))
return -1;
var torpedoConfig = _gameConfig.TorpedoConfigs.FirstOrDefault(c => c.Number == propNumber);
if (torpedoConfig == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.道具配置不存在, "鱼雷配置不存在");
return -1;
}
var player = room.Players.FirstOrDefault(p => p.UserId == userId);
if (player == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.用户不存在, "用户不存在");
return -1;
}
var baseProductCount = new BaseProductCount
{
Number = propNumber,
Name = torpedoConfig.Name,
Type = ProductTypeEnum.鱼雷,
Count = 1
};
_capPublisher.Publish(TopicKeys.用户资产变更, new UserAssetParam()
{
ChangeType = UserAssetChangeTypeEnum.减少,
UserId = userId,
Source = RewardSource.鱼雷,
RoomName = room.RoomName,
RoomInfo = room.RoomId,
RewardItems = new List<BaseProductCount>() { baseProductCount }
});
long randomGold = CalcTorpedoGold(torpedoConfig);
player.Gold += randomGold;
_redisService.SetGameRoom(room);
_sendResult.TorpedoResultNotice(roomId, userId, propNumber, randomGold, torpedoConfig.Color);
_hubGame.SendUserUserId(userId, "AssetUpdated", new
{
CurrencyType = 1,
ChangeType = 1,
Amount = randomGold,
NewGold = player.Gold,
NewDiamond = player.Diamond,
Source = RewardSource.鱼雷.ToString(),
Remark = ""
});
return player.Gold;
}
private long CalcTorpedoGold(TorpedoItemConfig config)
{
if (config.RewardRanges == null || config.RewardRanges.Count == 0)
return 0;
int totalWeight = config.RewardRanges.Sum(r => r.Weight);
if (totalWeight <= 0)
return 0;
int randomValue = GlobalRandom.Next(0, totalWeight);
int currentWeightSum = 0;
foreach (var range in config.RewardRanges)
{
currentWeightSum += range.Weight;
if (randomValue < currentWeightSum)
return GlobalRandom.NextInclusive(range.MinGold, range.MaxGold);
}
return 0;
}
public async Task ChangeGunMultiple(string cid, string roomId, string userId, int multiple)
{
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return;
}
var player = room.Players.Where(c => c.UserId == userId).FirstOrDefault();
if (player == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.用户不存在, "用户不存在");
return;
}
var roomConfig = _gameConfig?.RoomConfigs?.FirstOrDefault(c => c.GameType == room.GameType && c.RoomLevel == room.RoomLevel);
if (!IsCallerPlayer(cid, room, player) || roomConfig?.CannonMultipleList?.Contains(multiple) != true)
{
_sendResult.ErrorMessage(cid, ErrorCodes.炮倍参数无效, "连接用户或炮倍参数无效");
return;
}
player.CannonMultiple = multiple;
_redisService.SetGameRoom(room);
_sendResult.ChangeGunMultipleNotice(roomId, userId, multiple);
}
public async Task GetPlayerInfo(string cid,string roomId, string userId)
{
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return;
}
var callerUserId = _redisService.GetUserId(cid);
if (string.IsNullOrWhiteSpace(callerUserId)
|| !room.Players.Any(p => !p.IsBot && p.UserId == callerUserId && p.CId == cid && p.State == 0))
return;
var player = room.Players.Where(c => c.UserId == userId).FirstOrDefault();
if (player == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.用户不存在, "用户不存在");
return;
}
_sendResult.SendPlayerInfo(cid, player);
}
public async Task SetPlayerAfk(string cid, string roomId, string userId)
{
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return;
}
var player = room.Players.FirstOrDefault(p => p.UserId == userId && p.CId == cid);
if (player == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.用户不存在, "用户不存在");
return;
}
player.IsAfk = true;
_redisService.SetGameRoom(room);
_sendResult.PlayerAfkNotice(roomId, userId);
}
public async Task CancelPlayerAfk(string cid, string roomId, string userId)
{
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.房间不存在, "房间信息不存在");
return;
}
var player = room.Players.FirstOrDefault(p => p.UserId == userId && p.CId == cid);
if (player == null)
{
_sendResult.ErrorMessage(cid, ErrorCodes.用户不存在, "用户不存在");
return;
}
player.IsAfk = false;
_redisService.SetGameRoom(room);
_sendResult.PlayerCancelAfkNotice(roomId, userId);
}
public async Task<List<string>> BotFireCannon(string cid, string roomId, string botUserId, long gold, int count)
{
if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(roomId)
|| string.IsNullOrWhiteSpace(botUserId) || gold <= 0 || count <= 0 || count > 100)
return new List<string>();
var bulletIds = new List<string>();
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
return bulletIds;
}
var bot = room.Players?.FirstOrDefault(p => p.UserId == botUserId && p.IsBot);
if (bot == null)
{
return bulletIds;
}
if (gold <= int.MaxValue)
bot.CannonMultiple = (int)gold;
long totalCost;
try
{
totalCost = checked(gold * count);
}
catch (OverflowException)
{
return bulletIds;
}
if (bot.Gold < totalCost)
{
var position = bot.Position;
var exitConfig = _gameConfig?.BotConfig?.ExitCondition;
await RemoveBotFromRoom(
room,
bot,
exitConfig?.SendLeaveRoom ?? true,
exitConfig?.ShowWaitingAfterExit ?? false);
CreateReplacementBot(room, position);
_redisService.SetGameRoom(room);
return bulletIds;
}
bot.Gold -= totalCost;
for (int i = 0; i < count; i++)
bulletIds.Add(Guid.NewGuid().ToString("N"));
_redisService.SetGameRoom(room);
try
{
await _sendResult.RoomFireNoticeAsync(roomId, cid, botUserId, bulletIds, gold, count, null, true);
}
catch (Exception ex)
{
_logger.LogError(ex,
"机器人发炮广播失败 roomId={RoomId} botUserId={BotUserId} gold={Gold} count={Count} bulletCount={BulletCount}",
roomId, botUserId, gold, count, bulletIds.Count);
throw;
}
await CheckBotExit(room, bot);
return bulletIds;
}
public async Task BotKillFish(string cid, string roomId, string botUserId, string fishNumber, string fishId, long gold, string bulletId)
{
if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(roomId)
|| string.IsNullOrWhiteSpace(botUserId) || string.IsNullOrWhiteSpace(fishNumber)
|| string.IsNullOrWhiteSpace(fishId) || gold <= 0)
return;
var killConfig = GetFishingKillConfig();
var fishConfig = killConfig.FishTypeConfigs?.FirstOrDefault(f => f.FishNumber == fishNumber);
if (fishConfig == null)
return;
var room = _redisService.GetGameRoom(roomId);
if (room == null)
return;
var bot = room.Players?.FirstOrDefault(p => p.UserId == botUserId && p.IsBot);
if (bot == null)
return;
long? serverFishId = null;
if (long.TryParse(fishId, out var parsedFishId))
{
serverFishId = parsedFishId;
if (!_fishSpawnService.ValidateFishHit(roomId, parsedFishId))
return;
}
double killWeight = fishConfig.KillWeight;
if (Random(killWeight, 10000))
{
long skillGold;
try
{
skillGold = checked(fishConfig.FishScore * gold);
}
catch (OverflowException)
{
return;
}
(long gold, double beishu, object res) res = CalcSkillGold(killConfig.BossConfigs, fishNumber, (int)fishConfig.FishScore, skillGold, roomId, fishId);
bool finalKill = true;
if (fishNumber == "12030")
{
finalKill = res.res?.GetType().GetProperty("type")?.GetValue(res.res) is int t && t == 2;
}
else if (fishNumber == "12029" || fishNumber == "12034")
{
finalKill = res.res?.GetType().GetProperty("isKill")?.GetValue(res.res) is bool k && k;
}
if (finalKill)
{
var killClaimed = serverFishId.HasValue
? _fishSpawnService.TryMarkFishKilled(roomId, serverFishId.Value)
: fishConfig.FishType == FishTypeEnum.普通鱼
|| _fishKillTracker.TryAdd(roomId, fishId);
if (!killClaimed)
return;
}
skillGold = res.gold;
bot.Gold = skillGold > long.MaxValue - bot.Gold ? long.MaxValue : bot.Gold + skillGold;
_sendResult.RoomKillNotice(roomId, fishNumber, botUserId, skillGold, fishId, DateTime.Now.ToTimeStamp(), res.beishu, gold, fishConfig.FishScore, res.res);
await FishKillControl(cid, room, bot, fishNumber, skillGold, fishId, gold, DateTime.Now.ToTimeStamp(), killConfig);
}
_redisService.SetGameRoom(room);
await CheckBotExit(room, bot);
}
private bool IsCallerUser(string cid, string roomId, string userId)
{
if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(roomId) || string.IsNullOrWhiteSpace(userId))
return false;
var mappedUserId = _redisService.GetUserId(cid);
var userRedis = _redisService.GetRedisUserCId(userId);
return mappedUserId == userId && userRedis != null
&& userRedis.CId == cid && userRedis.RoomId == roomId;
}
private bool IsCallerPlayer(string cid, RedisGameRoom room, RedisGamePlayer player)
{
return room != null && player != null && !player.IsBot
&& player.CId == cid && player.State == 0
&& IsCallerUser(cid, room.RoomId, player.UserId);
}
private async Task ForceLeaveRoom(string roomId, string userId)
{
await LeaveRoomInternal(null, roomId, userId);
}
private async Task CheckBotExit(RedisGameRoom room, RedisGamePlayer bot)
{
if (bot == null || !bot.IsBot) return;
var cfg = _gameConfig?.BotConfig?.ExitCondition;
if (bot.Gold < bot.CannonMultiple)
{
var position = bot.Position;
await RemoveBotFromRoom(room, bot, cfg?.SendLeaveRoom ?? true, cfg?.ShowWaitingAfterExit ?? false);
CreateReplacementBot(room, position);
_redisService.SetGameRoom(room);
return;
}
if (cfg == null) return;
if (cfg.ExitWhenGoldAbove > 0 && bot.Gold > cfg.ExitWhenGoldAbove)
{
await RemoveBotFromRoom(room, bot, cfg.SendLeaveRoom, cfg.ShowWaitingAfterExit);
return;
}
if (cfg.ExitWhenGoldBelow > 0 && bot.Gold < cfg.ExitWhenGoldBelow)
{
await RemoveBotFromRoom(room, bot, cfg.SendLeaveRoom, cfg.ShowWaitingAfterExit);
}
}
private async Task RemoveBotFromRoom(RedisGameRoom room, RedisGamePlayer bot, bool sendLeaveRoom, bool showWaiting)
{
if (room?.Players == null || bot == null || !room.Players.Remove(bot))
return;
ClearBullets(room.RoomId, bot.UserId);
NormalizeRoomPlayerCounts(room);
_redisService.SetGameRoom(room);
if (sendLeaveRoom)
{
_sendResult.BotLeaveRoomNotice(room.RoomId, bot.UserId);
if (!string.IsNullOrWhiteSpace(bot.CId))
_hubGame.RemoveGroup(bot.CId, room.RoomId);
}
_hubGame.SendGroup(room.RoomId, "OnSeatChange", new
{
type = "leave",
userId = bot.UserId,
position = bot.Position,
showWaiting = showWaiting
});
}
private void CreateReplacementBot(RedisGameRoom room, int position)
{
if (_gameConfig?.BotConfig?.Enabled != true)
return;
if (room?.Players == null || room.Players.Any(p => p.Position == position))
return;
var roomConfig = _gameConfig?.RoomConfigs?
.FirstOrDefault(c => c.GameType == room.GameType && c.RoomLevel == room.RoomLevel);
var botProfiles = _botProfileRepo.FilterBy(p => true).ToList();
if (roomConfig?.CannonMultipleList == null || roomConfig.CannonMultipleList.Count == 0
|| botProfiles == null || botProfiles.Count == 0)
{
return;
}
var profile = botProfiles[System.Random.Shared.Next(botProfiles.Count)];
var cannonMultiple = roomConfig.CannonMultipleList[System.Random.Shared.Next(roomConfig.CannonMultipleList.Count)];
var maxMultiple = roomConfig.CannonMultipleList.Max();
var gold = maxMultiple * System.Random.Shared.Next(100, 501);
var props = _gameConfig?.PropConfig ?? new List<PropInfConfig>();
var cannons = props.Where(p => p.Type == ProductTypeEnum.炮台 && p.IsEnable && !p.Number.StartsWith("401")).ToList();
var wings = props.Where(p => p.Type == ProductTypeEnum.翅膀 && p.IsEnable && p.IsSale).ToList();
var warGods = props.Where(p => p.Type == ProductTypeEnum.战神 && p.IsEnable && p.IsSale).ToList();
var replacementId = Guid.NewGuid().ToString("N");
var replacementNumber = Common.Utils.RandomHelper.RandomNumbers(8);
var replacement = new RedisGamePlayer
{
UserId = replacementId,
UserNumber = replacementNumber,
UserName = profile.Name,
Avatar = profile.Avatar,
Level = System.Random.Shared.Next(1, 101),
IsBot = true,
Gold = gold,
Diamond = System.Random.Shared.Next(100, 5001),
Position = position,
CannonId = cannons.Count > 0 ? cannons[System.Random.Shared.Next(cannons.Count)].Number : string.Empty,
WingsId = wings.Count > 0 ? wings[System.Random.Shared.Next(wings.Count)].Number : string.Empty,
WarGodId = warGods.Count > 0 && System.Random.Shared.Next(5) == 0
? warGods[System.Random.Shared.Next(warGods.Count)].Number
: string.Empty,
CannonMultiple = cannonMultiple,
BeginCoin = gold,
BeginTime = DateTime.Now.ToTimeStamp(),
CId = string.Empty,
State = 0,
PlayerFactor = new PlayerFactor()
};
room.Players.Add(replacement);
NormalizeRoomPlayerCounts(room);
_sendResult.RoomJoinNotice(room.RoomId, replacement);
_hubGame.SendGroup(room.RoomId, "OnSeatChange", new
{
type = "join",
playerId = replacement.Position + 1,
position = replacement.Position,
playerName = replacement.UserName,
nickname = replacement.UserName,
gold = replacement.Gold,
diamond = replacement.Diamond,
vipLevel = replacement.VipLevel,
turretId = 1,
turretRate = 1,
turretLv = 1,
headId = 1,
roomRate = 1,
bulletPower = 1
});
}
private void CreateRoomBots(RedisGameRoom room, RoomConfig roomConfig)
{
if (_gameConfig?.BotConfig?.Enabled != true)
{
room.RobotCount = 0;
return;
}
var botProfiles = _botProfileRepo.FilterBy(p => true).ToList();
if (botProfiles == null || botProfiles.Count == 0)
{
return;
}
var maxMultiple = roomConfig.CannonMultipleList.Max();
var occupiedPositions = room.Players.Select(p => p.Position).ToHashSet();
var availablePositions = Enumerable.Range(0, room.MaxPlayerCount)
.Where(p => !occupiedPositions.Contains(p))
.OrderByDescending(p => p)
.ToList();
int botsToCreate = Math.Min(room.RobotCount, availablePositions.Count);
var cannons = _gameConfig.PropConfig.Where(p => p.Type == ProductTypeEnum.炮台 && p.IsEnable && !p.Number.StartsWith("401")).ToList();
var wings = _gameConfig.PropConfig.Where(p => p.Type == ProductTypeEnum.翅膀 && p.IsEnable && p.IsSale).ToList();
var warGods = _gameConfig.PropConfig.Where(p => p.Type == ProductTypeEnum.战神 && p.IsEnable && p.IsSale).ToList();
for (int i = 0; i < botsToCreate; i++)
{
var idx = System.Random.Shared.Next(botProfiles.Count);
var profile = botProfiles[idx];
var position = availablePositions[i];
var gold = maxMultiple * System.Random.Shared.Next(100, 501);
var cannonIndex = System.Random.Shared.Next(roomConfig.CannonMultipleList.Count);
var botId = Guid.NewGuid().ToString("N");
var botNumber = Common.Utils.RandomHelper.RandomNumbers(8);
var bot = new RedisGamePlayer
{
UserId = botId,
UserNumber = botNumber,
UserName = profile.Name,
Avatar = profile.Avatar,
Level = System.Random.Shared.Next(1, 101),
IsBot = true,
Gold = gold,
Diamond = System.Random.Shared.Next(100, 1001),
Position = position,
CannonId = cannons.Count > 0 ? cannons[System.Random.Shared.Next(cannons.Count)].Number : string.Empty,
WingsId = wings.Count > 0 ? wings[System.Random.Shared.Next(wings.Count)].Number : string.Empty,
WarGodId = warGods.Count > 0 && System.Random.Shared.Next(5) == 0
? warGods[System.Random.Shared.Next(warGods.Count)].Number
: string.Empty,
CannonMultiple = roomConfig.CannonMultipleList[cannonIndex],
BeginCoin = gold,
BeginTime = DateTime.Now.ToTimeStamp(),
CId = string.Empty,
State = 0,
PlayerFactor = new PlayerFactor()
};
room.Players.Add(bot);
_hubGame.SendGroup(room.RoomId, "OnSeatChange", new
{
type = "join",
playerId = bot.Position + 1,
position = bot.Position,
playerName = bot.UserName,
nickname = bot.UserName,
gold = bot.Gold,
diamond = bot.Diamond,
vipLevel = bot.VipLevel,
turretId = 1,
turretRate = 1,
turretLv = 1,
headId = 1,
roomRate = 1,
bulletPower = 1
});
}
NormalizeRoomPlayerCounts(room);
}
private static void NormalizeRoomPlayerCounts(RedisGameRoom room, bool preserveEmptyRoomRobotPlan = false)
{
if (room == null) return;
room.Players ??= new List<RedisGamePlayer>();
room.MaxPlayerCount = Math.Max(0, room.MaxPlayerCount);
room.CurrentOnlinePlayerCount = room.Players.Count(p => !p.IsBot && p.State == 0);
if (room.Players.Count > 0 || !preserveEmptyRoomRobotPlan)
room.RobotCount = room.Players.Count(p => p.IsBot);
else
room.RobotCount = Math.Clamp(room.RobotCount, 0, room.MaxPlayerCount);
var overflow = room.Players.Count - room.MaxPlayerCount;
if (overflow > 0)
{
var overflowBots = room.Players.Where(p => p.IsBot).Take(overflow).ToList();
foreach (var bot in overflowBots)
room.Players.Remove(bot);
room.CurrentOnlinePlayerCount = room.Players.Count(p => !p.IsBot && p.State == 0);
room.RobotCount = room.Players.Count(p => p.IsBot);
}
}
public async Task HandleDisconnect(string cid)
{
string userId = null;
try
{
userId = _redisService.GetUserId(cid);
if (string.IsNullOrWhiteSpace(userId))
{
return;
}
_redisService.RemoveUserId(cid);
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis == null)
{
return;
}
if (!string.IsNullOrWhiteSpace(userRedis.CId) && userRedis.CId != cid)
{
return;
}
var roomId = userRedis.RoomId;
if (string.IsNullOrWhiteSpace(roomId))
{
return;
}
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
userRedis.RoomId = "";
_redisService.SetRedisUserCId(userRedis);
return;
}
var player = room.Players.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (player == null)
{
userRedis.RoomId = "";
_redisService.SetRedisUserCId(userRedis);
return;
}
player.State = 1;
_redisService.SetGameRoom(room);
_redisService.SetRedisUserCId(userRedis);
if (room.CreateUserId == userId)
{
var nextOwner = room.Players.FirstOrDefault(p => !p.IsBot && p.State == 0 && p.UserId != userId);
if (nextOwner != null)
{
room.CreateUserId = nextOwner.UserId;
_redisService.SetGameRoom(room);
_sendResult.ChangeControllerUser(roomId, room.CreateUserId);
}
else
{
room.CreateUserId = "";
_redisService.SetGameRoom(room);
_sendResult.HostDisconnectNotice(roomId);
}
}
_sendResult.RoomDisconnectNotice(roomId, userId);
var hasOnlinePlayer = room.Players.Any(p => !p.IsBot && p.State == 0);
if (!hasOnlinePlayer)
{
}
var cts = new CancellationTokenSource();
if (PendingDisconnects.TryAdd(userId, cts))
{
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(DisconnectTimeoutSeconds), cts.Token);
if (!cts.Token.IsCancellationRequested)
await OnDisconnectTimeout(userId, roomId);
}
catch (TaskCanceledException)
{
}
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "断线处理异常 cid={Cid} userId={UserId}", cid, userId);
}
}
private async Task FishKillControl(string cid, RedisGameRoom room, RedisGamePlayer player, string fishNumber, long addgold, string fishId, long jiangold, long timespan, FishingKillConfig killConfig)
{
//await _distributedLock.TryExecuteAsync(
// $"{RedisKey.DistributedLockPrefix}room:kill:{roomId}:{userId}",
// () =>
// {
var fishConfig = killConfig?.FishTypeConfigs?.FirstOrDefault(c => c.FishNumber == fishNumber);
if (killConfig.BattleOrderIsOpen && fishConfig != null)
{
RecordFishingTaskProgress(room.RoomId, player.UserId, TaskEvent.赢取金币, addgold);
RecordFishingTaskProgress(room.RoomId, player.UserId, TaskEvent.捕获任意鱼, 1);
if (fishConfig.FishType == FishTypeEnum.Boss鱼 || fishConfig.FishType == FishTypeEnum.特殊Boss)
{
RecordFishingTaskProgress(room.RoomId, player.UserId, TaskEvent.捕获Boss鱼, 1);
}
if (fishConfig.FishType == FishTypeEnum.黄金鱼)
{
RecordFishingTaskProgress(room.RoomId, player.UserId, TaskEvent.捕获黄金鱼, 1);
}
}
var configuredMinGold = killConfig.KillConfig.GlobalAnnounceMinGold;
if (fishConfig != null && KillAnnouncePolicy.ShouldAnnounce(addgold, configuredMinGold))
{
var announceTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var gameTypeName = room.GameType.ToString();
_sendResult.KillNotice(fishNumber, player.UserName, addgold, fishConfig?.FishName ?? "",
player.CannonMultiple, gameTypeName, room.RoomName, announceTimestamp);
_capPublisher.Publish(TopicKeys.全局击杀公告, new KillAnnounceParam
{
UserName = player.UserName,
GameTypeName = gameTypeName,
RoomName = room.RoomName,
FishName = fishConfig.FishName,
Multiple = player.CannonMultiple,
Gold = addgold,
Timestamp = announceTimestamp
});
}
}
private bool Random(double probability, int Max)
{
int randomValue = System.Random.Shared.Next(0, Max);
return randomValue < probability;
}
private double ComputeSystemFactor()
{
var systemGold = GetSystemGoldDayCached();
var systemFactor = 1.0 + (systemGold - _gameConfig.KillConfig.DaySystemGold) / (_gameConfig.KillConfig.NormalizationFactorSystem * 1.0);
return Math.Clamp(systemFactor, _gameConfig.KillConfig.MinSystemModifier, _gameConfig.KillConfig.MaxSystemModifier);
}
private void SyncSystemFactorToRoom(RedisGameRoom room, double newSystemFactor)
{
double oldFactor = room.SystemFactor;
room.SystemFactor = newSystemFactor;
if (Math.Abs(newSystemFactor - oldFactor) >= 0.1)
{
_redisService.SetGameRoom(room);
}
}
private double CalcFactor(PlayerFactor factor, FishTypeEnum fishType, long currentBalance, long totalDeposits)
{
long actualProfit = currentBalance - (totalDeposits + factor.StartingBonus);
double rawPlayer = -actualProfit / (_gameConfig.KillConfig.NormalizationFactorPlayer * 1.0);
double playerModifier = 1.0 + rawPlayer;
playerModifier = Math.Clamp(playerModifier, _gameConfig.KillConfig.MinPlayerModifier, _gameConfig.KillConfig.MaxPlayerModifier);
double userfactor = 1;
if (factor.IsBlack)
{
userfactor = factor.ViolateValue;
}
else
{
double fishRate = 1.0;
if (factor.CompensateQuota > 0)
{
switch (fishType)
{
case FishTypeEnum.普通鱼:
fishRate = factor.CompensateRate.Ordinary;
break;
case FishTypeEnum.Boss鱼:
case FishTypeEnum.特殊Boss:
fishRate = factor.CompensateRate.BossRate;
break;
case FishTypeEnum.黄金鱼:
fishRate = factor.CompensateRate.GoldRate;
break;
case FishTypeEnum.特殊鱼:
fishRate = factor.CompensateRate.SpecialRate;
break;
}
playerModifier *= fishRate;
}
else if (factor.PenaltyQuota > 0)
{
switch (fishType)
{
case FishTypeEnum.普通鱼:
fishRate = factor.PenaltyRate.Ordinary;
break;
case FishTypeEnum.Boss鱼:
case FishTypeEnum.特殊Boss:
fishRate = factor.PenaltyRate.BossRate;
break;
case FishTypeEnum.黄金鱼:
fishRate = factor.PenaltyRate.GoldRate;
break;
case FishTypeEnum.特殊鱼:
fishRate = factor.PenaltyRate.SpecialRate;
break;
}
playerModifier *= fishRate;
}
playerModifier = Math.Clamp(playerModifier, _gameConfig.KillConfig.MinPlayerModifier, _gameConfig.KillConfig.MaxPlayerModifier);
userfactor = playerModifier;
}
var systemFactor = ComputeSystemFactor();
return userfactor * systemFactor;
}
private async Task TryCloseCompensateOnTargetAsync(RedisGameRoom room, RedisGamePlayer player, PlayerFactor playerFactor, string roomId, string userId)
{
if (playerFactor.CompensateQuota <= 0 || playerFactor.CompensateTargetGold <= 0 || player.Gold < playerFactor.CompensateTargetGold)
return;
var reachedGold = player.Gold;
var closedTarget = playerFactor.CompensateTargetGold;
playerFactor.CompensateQuota = 0;
playerFactor.CompensateTargetGold = 0;
playerFactor.CompensateStartGold = 0;
player.PlayerFactor = playerFactor;
_redisService.SetPlayerFactor(roomId, userId, playerFactor);
_redisService.SetGameRoom(room);
try
{
var user = await _userService.GetUser(userId);
if (user?.PlayerFactor != null)
{
user.PlayerFactor.CompensateQuota = 0;
user.PlayerFactor.CompensateTargetGold = 0;
user.PlayerFactor.CompensateStartGold = 0;
await _userService.UpdateUser(user);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "持久化补偿关闭失败 userId={UserId}", userId);
}
}
private void RecordFishingTaskProgress(string roomId, string userId, TaskEvent taskEvent, long count)
{
_redisService.AddFishingTaskProgress(roomId, userId, (int)taskEvent, count);
}
private async Task<long> TryFlushFishingSettlement(RedisGameRoom room, RedisGamePlayer player)
{
var progressRewardGold = await FlushProgressAndMergeGold(room.RoomId, player, player.CId);
try
{
var gameName = room.GameType switch
{
GameTypeEnum.风雷之渊 => "风雷之渊",
GameTypeEnum.永夜尸城 => "永夜尸城",
GameTypeEnum.中国象棋 => "中国象棋",
GameTypeEnum.星际迷航 => "星际迷航",
GameTypeEnum.黑神话悟空 => "黑神话悟空",
_ => room.GameType.ToString()
};
var settledGold = await _userService.GameCoinsSettlement(new GameCoinsSettlementParam
{
UserId = player.UserId,
BeforeGold = checked(player.BeginCoin + progressRewardGold),
AfterGold = player.Gold,
RoomSessionId = player.BeginTime,
RoomInfo = room.RoomId,
RoomName = $"{gameName}-{room.RoomName}"
});
return settledGold;
}
catch (Exception ex)
{
_logger.LogError(ex, "离房金币结算落库失败 roomId={RoomId} userId={UserId}", room.RoomId, player.UserId);
throw;
}
}
private async Task TryFlushFishingProgress(string roomId, string userId, string userName)
{
try
{
await FlushFishingRuntimeProgress(roomId, userId, userName);
_redisService.ClearFishingRuntimeProgress(roomId, userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "离房经验或消耗结算失败,保留Redis待下次重试 roomId={RoomId} userId={UserId}", roomId, userId);
}
try
{
FlushFishingTaskProgress(roomId, userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "离房任务结算失败,保留Redis待下次重试 roomId={RoomId} userId={UserId}", roomId, userId);
}
}
private async Task<long> FlushProgressAndMergeGold(string roomId, RedisGamePlayer player, string cid = null)
{
var before = await _userService.GetUser(player.UserId);
var goldBeforeProgress = before?.Assets?.Gold ?? 0;
await TryFlushFishingProgress(roomId, player.UserId, player.UserName);
var after = await _userService.GetUser(player.UserId);
var goldAfterProgress = after?.Assets?.Gold ?? goldBeforeProgress;
var progressRewardGold = Math.Max(0L, goldAfterProgress - goldBeforeProgress);
if (progressRewardGold <= 0)
return 0;
player.Gold = checked(player.Gold + progressRewardGold);
if (player.Gold > player.MaxGold) player.MaxGold = player.Gold;
return progressRewardGold;
}
private async Task FlushFishingRuntimeProgress(string roomId, string userId, string userName)
{
var exp = _redisService.GetFishingExpProgress(roomId, userId);
if (exp > 0)
{
(bool isok, string msg) updateResult = default;
const int maxAttempts = 5;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
updateResult = await _userService.UpdateAssets(new UserAssetParam
{
UserId = userId,
Exp = exp,
ChangeType = UserAssetChangeTypeEnum.增加,
Source = RewardSource.击杀鱼,
Remark = "捕鱼经验结算",
RoomInfo = roomId,
IsAddRecord = false
});
if (updateResult.isok)
break;
if (attempt < maxAttempts)
await Task.Delay(100 * attempt);
}
if (!updateResult.isok)
throw new InvalidOperationException(
$"捕鱼经验入库失败,Redis进度已保留。roomId={roomId}, userId={userId}, exp={exp}, message={updateResult.msg}");
}
var consume = _redisService.GetFishingConsumeProgress(roomId, userId);
if (consume > 0)
{
await _userService.RecordFishingConsume(consume, new CommonInfrastructure.Response.LoginUser
{
UserId = userId,
UserName = userName ?? userId
});
}
}
private void FlushFishingTaskProgress(string roomId, string userId)
{
var taskEvents = Enum.GetValues<TaskEvent>().Select(value => (int)value).ToArray();
var progress = _redisService.GetFishingTaskProgress(roomId, userId, taskEvents);
foreach (var item in progress.Where(item => item.Value > 0 && Enum.IsDefined(typeof(TaskEvent), item.Key)))
{
_capPublisher.Publish(TopicKeys.任务完成, new CompletedTaskParam
{
count = item.Value,
userId = userId,
taskEvent = (TaskEvent)item.Key
});
}
_redisService.ClearFishingTaskProgress(roomId, userId, taskEvents);
}
public (long gold,double beishu, object res) CalcSkillGold(BossConfig bossConfig, string fishNumber,int socre, long newgold,string roomId,string fishId)
{
var gold = newgold;
if (bossConfig.LuckWheel.Any(d=>d.FishNumber.Any(c => c == fishNumber)))
{
var luckWheel = bossConfig.LuckWheel.FirstOrDefault(d => d.FishNumber.Any(c => c == fishNumber));
var res = luckWheel.GetResponse();
var beishu = Convert.ToInt32(res.Sum(c => c.Score));
gold =gold * beishu;
return (gold, beishu* socre, new { res,count=res.Count });
}
else if (bossConfig.LuckPoker.FishNumber.Any(c => c == fishNumber))
{
var res = bossConfig.LuckPoker.GetResponse();
var beishu = 1;
if(res!=null && res.Count()>0)
beishu= Convert.ToInt32(res[0].Score);
gold = gold * beishu;
return (gold, beishu * socre, new { res, count = res.Count });
}
else if (bossConfig.LuckFlyDisk.Any(c => c.FishNumber.Any(f => f == fishNumber)))
{
var luckFlyDisk = bossConfig.LuckFlyDisk.FirstOrDefault(d => d.FishNumber.Any(c => c == fishNumber));
var res = luckFlyDisk.GetResponse();
var beishu = res.Select(c => c.Score).Aggregate(1.0, (acc, x) => acc * x);
gold = Convert.ToInt64(gold * beishu);
return (gold, beishu * socre, new { res, count = res.Count });
}
else if (bossConfig.FullScreen.FishNumber.Any(c => c == fishNumber))
{
return (gold, socre, null);
}
else if (bossConfig.Range.FishNumber.Any(c => c == fishNumber))
{
return (gold, socre, null);
}
else if (bossConfig.Probability.Any(d=>d.FishNumber.Any(c => c == fishNumber)))
{
var probability = bossConfig.Probability.FirstOrDefault(d => d.FishNumber.Any(c => c == fishNumber));
var res = probability.GetResponse();
if ( res> 0)
gold = Convert.ToInt64(gold * res);
return (gold, res * socre, res);
}
else if (bossConfig.InterstellarCruise.FishNumber.Any(c => c == fishNumber))
{
var res = bossConfig.InterstellarCruise.GetResponse(fishId);
gold = Convert.ToInt64(gold * res.beishu);
return (gold, res.beishu * socre, new
{
res.isKill,
res.beishu,
res.fishId,
res.animId
});
}
else if (bossConfig.FlameStorm.FishNumber.Any(c => c == fishNumber))
{
var res = bossConfig.FlameStorm.GetResponse(fishId);
gold = Convert.ToInt64(gold * res.beishu);
return (gold, res.beishu * socre, new
{
res.isKill,
res.beishu,
res.fishId,
res.animId
});
}
else if (bossConfig.Bomb.FishNumber.Any(c => c == fishNumber))
{
var res = bossConfig.Bomb.GetResponse(fishId);
gold = Convert.ToInt64(gold * res.beishu);
return (gold, res.beishu * socre, new
{
res.beishu,
res.type
});
}
else if (bossConfig.Scorpion.FishNumber.Any(c => c == fishNumber))
{
var res = bossConfig.Scorpion.GetResponse();
gold = Convert.ToInt64(gold * res.beishu);
return (gold, res.beishu * socre, new
{
res.beishu,
res.items
});
}
else if (bossConfig.LuckDragon.Any(c => c.FishNumber.Contains(fishNumber)))
{
var dragonConfig = bossConfig.LuckDragon.FirstOrDefault(c => c.FishNumber.Contains(fishNumber));
if (dragonConfig != null)
{
var res = dragonConfig.GetResponse();
gold = Convert.ToInt64(gold * res.beizhu);
return (gold, res.beizhu * socre, new
{
res.option,
res.beizhu,
res.ringName,
msg = $"神龙转盘·{res.ringName},倍数x{res.beizhu}"
});
}
}
return (gold, socre, null);
}
public async Task SendRoomChange(RedisGameRoom room)
{
if (room == null)
return;
await _capPublisher.PublishAsync(TopicKeys.房间变化, room);
}
private bool VerifySign(string cid, string rawStr, long timestamp, string sign)
{
var serverNow = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(serverNow - timestamp) > _bulletConfig.TimeWindowSeconds)
{
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.请求已过期, "请求已过期");
return false;
}
if (!SignHelper.Verify(rawStr, _bulletConfig.SignKey, sign))
{
if (!string.IsNullOrWhiteSpace(cid))
_sendResult.ErrorMessage(cid, ErrorCodes.签名错误, "签名错误");
return false;
}
return true;
}
public Task CancelPendingDisconnect(string userId)
{
if (PendingDisconnects.TryRemove(userId, out var cts))
{
try { cts.Cancel(); cts.Dispose(); }
catch (Exception ex)
{
_logger.LogError(ex, "取消并释放断线定时器失败 userId={UserId}", userId);
}
}
return Task.CompletedTask;
}
private async Task OnDisconnectTimeout(string userId, string roomId)
{
await _distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
() => OnDisconnectTimeoutInternal(userId, roomId),
retry: true);
}
private async Task OnDisconnectTimeoutInternal(string userId, string roomId)
{
PendingDisconnects.TryRemove(userId, out _);
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis == null || userRedis.RoomId != roomId)
{
return;
}
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
userRedis.RoomId = "";
userRedis.CId = "";
_redisService.SetRedisUserCId(userRedis);
return;
}
var player = room.Players.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (player == null || player.State != 1)
{
return;
}
try
{
var progressRewardGold = await FlushProgressAndMergeGold(roomId, player, player.CId);
await _userService.GameCoinsSettlement(new GameCoinsSettlementParam
{
UserId = userId,
BeforeGold = checked(player.BeginCoin + progressRewardGold),
AfterGold = player.Gold,
RoomSessionId = player.BeginTime,
RoomInfo = roomId,
RoomName = $"{room.GameType}-{room.RoomName}"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "断线超时结算失败 userId={UserId} roomId={RoomId}", userId, roomId);
}
try
{
await CleanupRoomAsync(userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "断线超时清理房间异常 userId={UserId} roomId={RoomId}", userId, roomId);
await ForceCleanupPlayer(userId, roomId);
}
}
private async Task SettleAndKickAllPlayers(RedisGameRoom room)
{
var roomId = room.RoomId;
var realPlayers = room.Players.Where(p => !p.IsBot).ToList();
foreach (var player in realPlayers)
{
var userId = player.UserId;
CancelPendingDisconnect(userId);
try
{
var progressRewardGold = await FlushProgressAndMergeGold(roomId, player, player.CId);
await _userService.GameCoinsSettlement(new GameCoinsSettlementParam
{
UserId = userId,
BeforeGold = checked(player.BeginCoin + progressRewardGold),
AfterGold = player.Gold,
RoomSessionId = player.BeginTime,
RoomInfo = roomId,
RoomName = $"{room.GameType}-{room.RoomName}"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "全员结算失败 userId={UserId} roomId={RoomId}", userId, roomId);
}
_redisService.ClearPlayerRuntimeData(roomId, userId);
PlayerFactorCache.TryRemove($"{roomId}|{userId}", out _);
_bulletTracker.Clear(roomId, userId);
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis != null)
{
userRedis.RoomId = "";
userRedis.CId = "";
userRedis.PartitionId = "";
_redisService.SetRedisUserCId(userRedis);
}
room.Players.Remove(player);
_sendResult.RoomLeaveNotice(roomId, userId);
}
int savedBotCount = room.RobotCount;
room.Players.Clear();
room.CreateUserId = "";
room.CurrentOnlinePlayerCount = 0;
room.RobotCount = savedBotCount > 0 ? savedBotCount : room.RobotCount;
ClearKillFishIds(roomId);
_fishSpawnService.UnregisterRoom(roomId);
NormalizeRoomPlayerCounts(room, preserveEmptyRoomRobotPlan: true);
_redisService.SetGameRoom(room);
await SendRoomChange(room);
}
#region IPendingSettlementService
public async Task<string> SettleGoldAsync(string userId)
{
var session = _redisService.GetRedisUserCId(userId);
var roomId = session?.RoomId ?? "none";
return await _distributedLock.TryExecuteAsync(
$"{RedisKey.DistributedLockPrefix}fish:gold:{roomId}:{userId}",
() => SettleGoldInternalAsync(userId),
retry: true);
}
private async Task<string> SettleGoldInternalAsync(string userId)
{
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis == null || string.IsNullOrWhiteSpace(userRedis.RoomId))
{
return string.Empty;
}
var roomId = userRedis.RoomId;
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
userRedis.RoomId = "";
userRedis.CId = "";
_redisService.SetRedisUserCId(userRedis);
return string.Empty;
}
var player = room.Players.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (player == null)
{
userRedis.RoomId = "";
userRedis.CId = "";
_redisService.SetRedisUserCId(userRedis);
return string.Empty;
}
var progressRewardGold = await FlushProgressAndMergeGold(roomId, player, player.CId);
var settledGold = await _userService.GameCoinsSettlement(new GameCoinsSettlementParam
{
UserId = userId,
BeforeGold = checked(player.BeginCoin + progressRewardGold),
AfterGold = player.Gold,
RoomSessionId = player.BeginTime,
RoomInfo = roomId,
RoomName = $"{room.GameType}-{room.RoomName}"
});
await CleanupRoomAsync(userId, force: true);
return roomId;
}
public async Task CleanupRoomAsync(string userId, bool force = false)
{
CancelPendingDisconnect(userId);
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis == null || string.IsNullOrWhiteSpace(userRedis.RoomId))
{
return;
}
var roomId = userRedis.RoomId;
var room = _redisService.GetGameRoom(roomId);
if (room == null)
{
userRedis.RoomId = "";
userRedis.CId = "";
_redisService.SetRedisUserCId(userRedis);
return;
}
var player = room.Players.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (player == null)
{
userRedis.RoomId = "";
userRedis.CId = "";
_redisService.SetRedisUserCId(userRedis);
return;
}
if (player.State == 0 && !force)
{
return;
}
try
{
_redisService.ClearPlayerRuntimeData(roomId, userId);
}
catch (Exception ex) { _logger.LogError(ex, "CleanupRoomAsync ClearPlayerRuntimeData 失败 userId={UserId}", userId); }
PlayerFactorCache.TryRemove($"{roomId}|{userId}", out _);
try
{
_bulletTracker.Clear(roomId, userId);
}
catch (Exception ex) { _logger.LogError(ex, "CleanupRoomAsync _bulletTracker.Clear 失败 userId={UserId}", userId); }
room.Players.Remove(player);
var leftPosition = player.Position;
if (room.CreateUserId == userId)
{
var nextOwner = room.Players.FirstOrDefault(p => !p.IsBot);
room.CreateUserId = nextOwner?.UserId ?? "";
if (!string.IsNullOrWhiteSpace(room.CreateUserId))
_sendResult.ChangeControllerUser(roomId, room.CreateUserId);
}
userRedis.RoomId = "";
userRedis.CId = "";
userRedis.PartitionId = "";
try
{
_redisService.SetRedisUserCId(userRedis);
}
catch (Exception ex)
{
_logger.LogError(ex, "CleanupRoomAsync SetRedisUserCId 失败 userId={UserId}", userId);
}
try
{
var user = await _userService.GetUser(userId);
if (user != null)
{
user.Static ??= new StaticInfo();
user.Static.RoomNumber = string.Empty;
user.Static.CurrentGameType = 0;
await _userService.UpdateUser(user);
}
}
catch (Exception ex) { _logger.LogError(ex, "CleanupRoomAsync 清除用户房间信息失败 userId={UserId}", userId); }
NormalizeRoomPlayerCounts(room);
try
{
_redisService.SetGameRoom(room);
}
catch (Exception ex)
{
_logger.LogError(ex, "CleanupRoomAsync SetGameRoom 失败 roomId={RoomId}", roomId);
}
try
{
_hubGame.SendGroup(roomId, "OnSeatChange", new
{
type = "leave",
userId = userId,
playerId = leftPosition + 1,
position = leftPosition,
showWaiting = false
});
}
catch (Exception ex)
{
_logger.LogError(ex, "CleanupRoomAsync OnSeatChange 失败 roomId={RoomId} userId={UserId}", roomId, userId);
}
try
{
_sendResult.RoomLeaveNotice(roomId, userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "CleanupRoomAsync RoomLeaveNotice 失败 roomId={RoomId} userId={UserId}", roomId, userId);
}
try
{
await SendRoomChange(room);
}
catch (Exception ex)
{
_logger.LogError(ex, "CleanupRoomAsync SendRoomChange 失败 roomId={RoomId}", roomId);
}
if (!room.Players.Any(p => !p.IsBot))
{
_fishSpawnService.UnregisterRoom(roomId);
}
}
public List<ActiveFish> GetActiveFishes(string roomId)
{
return _fishSpawnService.GetActiveFishes(roomId);
}
private async Task ForceCleanupPlayer(string userId, string roomId)
{
try
{
var userRedis = _redisService.GetRedisUserCId(userId);
if (userRedis != null)
{
userRedis.RoomId = "";
userRedis.CId = "";
userRedis.PartitionId = "";
_redisService.SetRedisUserCId(userRedis);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "ForceCleanupPlayer 清除 RedisUserCId 失败 userId={UserId}", userId);
}
try
{
var room = _redisService.GetGameRoom(roomId);
if (room != null)
{
var player = room.Players.FirstOrDefault(p => p.UserId == userId && !p.IsBot);
if (player != null)
{
var leftPosition = player.Position;
room.Players.Remove(player);
_redisService.SetGameRoom(room);
_hubGame.SendGroup(roomId, "OnSeatChange", new
{
type = "leave",
userId = userId,
playerId = leftPosition + 1,
position = leftPosition,
showWaiting = false
});
}
else
{
_redisService.SetGameRoom(room);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "ForceCleanupPlayer 移除玩家失败 userId={UserId} roomId={RoomId}", userId, roomId);
}
try
{
_sendResult.RoomLeaveNotice(roomId, userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "ForceCleanupPlayer 发送 RoomLeaveNotice 失败 userId={UserId} roomId={RoomId}", userId, roomId);
}
}
private void SendCurrentFishState(string cid, string roomId)
{
try
{
var activeFishes = _fishSpawnService.GetActiveFishes(roomId);
if (activeFishes == null || activeFishes.Count == 0)
{
return;
}
var payload = new
{
type = "spawn",
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
fish = 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, (f.ExpireAt - DateTime.Now).TotalSeconds)
}).ToList()
};
_hubGame.SendUserCId(cid, "OnFishSpawn", payload);
}
catch (Exception ex)
{
_logger.LogError(ex, "[JoinRoom] 同步鱼状态失败: cid={Cid} roomId={RoomId}", cid, roomId);
}
}
#endregion
}
}