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 _logger; private readonly IHubSend _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 _botProfileRepo; private readonly IBulletTracker _bulletTracker; private readonly IFishKillTracker _fishKillTracker; private readonly IHubContext _hubLoginContext; private readonly FishSpawnService _fishSpawnService; private static readonly ConcurrentDictionary PendingDisconnects = new(); private static readonly int DisconnectTimeoutSeconds = 60; private static readonly ConcurrentDictionary 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 _lastFactorPersist = new(); private static readonly TimeSpan FactorPersistInterval = TimeSpan.FromSeconds(30); public FishingGameService(ICapPublisher capPublisher, IRedisService redisService, IHubSend hubGame, IFishingSendService sendResult, IUserService userService, IMapper mapper, IDistributedLock distributedLock, BulletConfig bulletConfig, ILogger logger, IMongoBaseRepository botProfileRepo, IBulletTracker bulletTracker, IFishKillTracker fishKillTracker, IHubContext 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(), 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(json, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } else { _gameConfig = new GameConfig { RoomConfigs = new List { new RoomConfig { RoomNumber = "1", RoomName = "新手渔场", GameType = GameTypeEnum.风雷之渊, RoomLevel = 1, RoomPartitionNum = 1, RoomCount = 1, RoomGoldLimit = 0, CannonMultipleList = new List { 10, 20, 50, 100, 200 } } }, FishRoomConfig = new FishRoomConfig { SeatSelectLimit = new List { 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 { 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(); 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(); 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(); 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 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 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 { 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; 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 { 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); } /// /// 击杀经验实时入库成功后,同步房间内玩家的经验/等级/经验上限(与 UserEntity.LevelVerify 的升级判定保持一致)。 /// 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 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 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 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 bullets, long gold)> FireCannonInternal(string cid, string roomId, string userId, long gold, int count, long timestamp, string sign, object extra) { var result = new List(); 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 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(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 } }); } 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 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 } }); 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> 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(); var bulletIds = new List(); 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(); 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(); 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 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 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().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 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 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 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 } }