3158 lines
128 KiB
C#
3158 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;
|
|
}
|
|
}
|
|
|
|
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,
|
|
SyncToFrontend = 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);
|
|
}
|
|
}
|
|
|
|
_sendResult.RoomKillNotice(roomId, fishNumber, player.UserId, skillGold, fihsId, timespan,res.beishu, gold,fishConfig.FishScore, res.res, gainExp: killExpGain, exp: player.Exp, level: player.Level, expMax: player.MaxExp);
|
|
await FishKillControl(cid, gameRoom, player, fishNumber, skillGold, fihsId,gold,timespan, killConfig);
|
|
|
|
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 }, exp: player.Exp, level: player.Level, expMax: player.MaxExp);
|
|
}
|
|
}
|
|
|
|
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 }, exp: player.Exp, level: player.Level, expMax: player.MaxExp);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 机器人击杀累计经验并在游戏内升级(按 UserLevelConfigs 判定,等级上限与经验上限封顶)。
|
|
/// </summary>
|
|
private void AddBotExp(RedisGamePlayer bot, long gainExp)
|
|
{
|
|
if (bot == null || gainExp <= 0)
|
|
return;
|
|
|
|
bot.Exp = bot.Exp > long.MaxValue - gainExp ? long.MaxValue : bot.Exp + gainExp;
|
|
|
|
var configs = _gameConfig?.UserLevelConfigs;
|
|
if (configs == null || configs.Count == 0)
|
|
return;
|
|
|
|
var levelCfg = configs.FirstOrDefault(c => bot.Exp >= c.ExpMin && bot.Exp < c.ExpMax)
|
|
?? configs.OrderBy(c => c.Level).LastOrDefault();
|
|
|
|
if (levelCfg == null)
|
|
return;
|
|
|
|
bot.Level = levelCfg.Level;
|
|
bot.MaxExp = levelCfg.ExpMax;
|
|
if (bot.Exp >= levelCfg.ExpMax)
|
|
bot.Exp = levelCfg.ExpMax - 1;
|
|
}
|
|
|
|
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;
|
|
|
|
var botExpGain = fishConfig.FishType is FishTypeEnum.Boss鱼 or FishTypeEnum.特殊鱼 or FishTypeEnum.特殊Boss
|
|
? 1000 : fishConfig.FishScore;
|
|
AddBotExp(bot, botExpGain);
|
|
|
|
_sendResult.RoomKillNotice(roomId, fishNumber, botUserId, skillGold, fishId, DateTime.Now.ToTimeStamp(), res.beishu, gold, fishConfig.FishScore, res.res, gainExp: botExpGain, exp: bot.Exp, level: bot.Level, expMax: bot.MaxExp);
|
|
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 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,
|
|
IsBot = true,
|
|
Gold = gold,
|
|
Diamond = System.Random.Shared.Next(100, 5001),
|
|
Position = position,
|
|
CannonMultiple = cannonMultiple,
|
|
BeginCoin = gold,
|
|
BeginTime = DateTime.Now.ToTimeStamp(),
|
|
CId = string.Empty,
|
|
State = 0,
|
|
PlayerFactor = new PlayerFactor()
|
|
};
|
|
RoomBotFactory.ApplyBotBaseValues(replacement, _gameConfig);
|
|
|
|
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);
|
|
|
|
|
|
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,
|
|
IsBot = true,
|
|
Gold = gold,
|
|
Diamond = System.Random.Shared.Next(100, 1001),
|
|
Position = position,
|
|
CannonMultiple = roomConfig.CannonMultipleList[cannonIndex],
|
|
BeginCoin = gold,
|
|
BeginTime = DateTime.Now.ToTimeStamp(),
|
|
CId = string.Empty,
|
|
State = 0,
|
|
PlayerFactor = new PlayerFactor()
|
|
};
|
|
RoomBotFactory.ApplyBotBaseValues(bot, _gameConfig);
|
|
|
|
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
|
|
}
|
|
}
|