using Microsoft.Extensions.Logging; using Yuanshen.Common.Data.Excel; using Yuanshen.Gameserver.Database; using Yuanshen.Gameserver.Network.Session; using Yuanshen.Protocol; namespace Yuanshen.Gameserver.Game; internal sealed class GmCommandService { private const int TeamSize = 4; private const uint MoraItemId = 202; private const uint MaxConstellation = 6; private readonly Player _player; private readonly GameState _state; private readonly SceneManager _scene; private readonly NetSession _session; private readonly PlayerPersistence _persistence; private readonly ExcelTableCollection _excel; private readonly WorldSpawner _worldSpawner; private readonly GachaBannerTable _banners; private readonly ILogger _logger; public GmCommandService(Player player, GameState state, SceneManager scene, NetSession session, PlayerPersistence persistence, ExcelTableCollection excel, WorldSpawner worldSpawner, GachaBannerTable banners, ILogger logger) { _player = player; _state = state; _scene = scene; _session = session; _persistence = persistence; _excel = excel; _worldSpawner = worldSpawner; _banners = banners; _logger = logger; } public async ValueTask> ExecuteAsync(string input) { string[] parts = input.TrimStart('/').Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length == 0) return Help(); string command = parts[0].ToLowerInvariant(); string[] args = parts[1..]; try { List reply = command switch { "help" => Help(), "tp" => await TeleportAsync(args), "spawn" => await SpawnAsync(args), "clear" => await ClearAsync(), "respawn" => await RespawnAsync(), "give" => await GiveAsync(args), "heal" => await HealAsync(), "level" => await LevelAsync(args), "asc" => await AscendAsync(args), "talent" => await TalentAsync(args), "const" => await ConstellationAsync(args), "wlevel" => await WeaponLevelAsync(args), "refine" => await RefineAsync(args), "wish" => await WishAsync(args), "avatar" => await AvatarAsync(args), "team" => await TeamAsync(args), "monster" => await SpawnAsync(args), "trifle" => await GiveAsync(args), "kill" => await KillAsync(args), "item" => await ItemAsync(args), "equip" => await EquipAsync(args), "break" => await AscendAsync(args), "skill" => await SkillAsync(args), "goto" => await TeleportAsync(args), "scoin" => await ScoinAsync(args), "hcoin" => await HcoinAsync(args), "player" => await PlayerAsync(args), "quest" => Unsupported("quest"), "dungeon" => Unsupported("dungeon"), "jump" => Unsupported("jump"), _ => new List { $"unknown command /{command} - try /help" } }; _logger.LogInformation("GM /{command} -> {result}", command, string.Join(" | ", reply)); return reply; } catch (Exception exception) { _logger.LogWarning("GM /{command} failed: {message}", command, exception.Message); return new List { $"/{command} failed: {exception.Message}" }; } } private static List Help() => new() { "commands: /tp x y z, /spawn id [n], /clear, /respawn, /give id [n], /heal,", "/level n, /asc n, /talent n, /const n, /wlevel n, /refine n, /wish [n] (grants fates),", "team: avatar add id, team add id, team remove id, team list", "mihoyo syntax: item add id [n], equip add id [lvl] [promote], level n, break n,", "skill slot n, talent unlock all, goto x y z, scoin n, hcoin n, player level n" }; private async ValueTask> AvatarAsync(string[] args) { if (args.Length >= 2 && args[0].Equals("add", StringComparison.OrdinalIgnoreCase)) return await TeamAddAsync(args[1]); return new List { "usage: avatar add avatarId" }; } private async ValueTask> TeamAsync(string[] args) { if (args.Length == 0) return TeamList(); string action = args[0].ToLowerInvariant(); if (action == "list") return TeamList(); if (action == "reset") return await TeamResetAsync(); if (args.Length < 2) return new List { "usage: team add|remove avatarId, team list, team reset" }; return action switch { "add" => await TeamAddAsync(args[1]), "remove" => await TeamRemoveAsync(args[1]), _ => new List { "usage: team add|remove avatarId, team list, team reset" } }; } private List TeamList() => _player.TeamAvatars .Select(avatar => $"{avatar.AvatarId} guid={avatar.Guid}") .DefaultIfEmpty("team is empty") .ToList(); private async ValueTask> TeamAddAsync(string rawId) { if (!uint.TryParse(rawId, out uint avatarId)) return new List { "usage: team add avatarId" }; GameAvatar? avatar = _player.Avatars.FirstOrDefault(candidate => candidate.AvatarId == avatarId); if (avatar is null) { List granted = await GrantAvatarAsync(avatarId, avatarId); avatar = _player.Avatars.FirstOrDefault(candidate => candidate.AvatarId == avatarId); if (avatar is null) return granted; } if (_player.TeamAvatarGuids.Contains(avatar.Guid)) return new List { $"avatar {avatarId} is already in the team" }; if (_player.TeamAvatarGuids.Count >= TeamSize) return new List { $"team is full, remove one first (max {TeamSize})" }; List team = _player.TeamAvatarGuids.Append(avatar.Guid).ToList(); await _scene.SetupTeamAsync(_player.CurrentTeamId, team, _player.CurrentAvatarGuid); await _persistence.SaveAsync(); return new List { $"added avatar {avatarId} to the team" }; } private async ValueTask> TeamResetAsync() { _player.ResetTeam(); await _scene.SetupTeamAsync(_player.CurrentTeamId, _player.TeamAvatarGuids.ToList(), _player.CurrentAvatarGuid); await _persistence.SaveAsync(); return new List { "team reset to the default avatar" }; } private async ValueTask> TeamRemoveAsync(string rawId) { if (!uint.TryParse(rawId, out uint avatarId)) return new List { "usage: team remove avatarId" }; GameAvatar? avatar = _player.Avatars.FirstOrDefault(candidate => candidate.AvatarId == avatarId); if (avatar is null || !_player.TeamAvatarGuids.Contains(avatar.Guid)) return new List { $"avatar {avatarId} is not in the team" }; if (_player.TeamAvatarGuids.Count <= 1) return new List { "cannot remove the last team member" }; List team = _player.TeamAvatarGuids.Where(guid => guid != avatar.Guid).ToList(); ulong active = _player.CurrentAvatarGuid == avatar.Guid ? team[0] : _player.CurrentAvatarGuid; await _scene.SetupTeamAsync(_player.CurrentTeamId, team, active); await _persistence.SaveAsync(); return new List { $"removed avatar {avatarId} from the team" }; } private static List Unsupported(string verb) => new() { $"{verb} is not implemented on this server yet" }; private async ValueTask> ItemAsync(string[] args) { if (args.Length >= 2 && args[0].Equals("add", StringComparison.OrdinalIgnoreCase)) return await GiveAsync(args[1..]); return new List { "usage: item add itemId [count]" }; } private async ValueTask> ScoinAsync(string[] args) { if (args.Length < 1 || !uint.TryParse(args[0], out uint amount)) return new List { "usage: scoin amount" }; _player.Scoin += amount; await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps()); await _persistence.SaveAsync(); return new List { $"mora = {_player.Scoin}" }; } private async ValueTask> HcoinAsync(string[] args) { if (args.Length < 1 || !uint.TryParse(args[0], out uint amount)) return new List { "usage: hcoin amount" }; _player.Hcoin += amount; await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps()); await _persistence.SaveAsync(); return new List { $"primogems = {_player.Hcoin}" }; } private async ValueTask> PlayerAsync(string[] args) { if (args.Length < 2 || !args[0].Equals("level", StringComparison.OrdinalIgnoreCase) || !uint.TryParse(args[1], out uint level)) { return new List { "usage: player level n" }; } level = Math.Clamp(level, 1u, 60u); await _session.NotifyAsync(CmdType.PlayerPropNotify, new PlayerPropNotify { PropMap = { { 4001u, new PropValue { Type = 4001, Ival = level, Val = level } } } }); return new List { $"player level = {level} (display only, not persisted)" }; } private async ValueTask> SkillAsync(string[] args) { if (args.Length < 2 || !uint.TryParse(args[1], out uint level)) return new List { "usage: skill slot level" }; return await TalentAsync(new[] { level.ToString() }); } private async ValueTask> EquipAsync(string[] args) { if (args.Length < 2 || !args[0].Equals("add", StringComparison.OrdinalIgnoreCase) || !uint.TryParse(args[1], out uint itemId)) { return new List { "usage: equip add itemId [level] [promote]" }; } GameAvatar? avatar = _player.ActiveAvatar; if (avatar is null) return new List { "no active avatar" }; uint level = args.Length > 2 && uint.TryParse(args[2], out uint parsedLevel) ? parsedLevel : 90u; uint promote = args.Length > 3 && uint.TryParse(args[3], out uint parsedPromote) ? parsedPromote : 0u; bool isWeapon = _excel.GetTable(ExcelType.Weapon).Items.OfType().Any(weapon => weapon.Id == itemId); if (isWeapon) { GameWeapon weapon = _player.CreateWeapon(itemId); weapon.Level = Math.Clamp(level, 1u, 90u); weapon.PromoteLevel = Math.Clamp(promote, 0u, 6u); avatar.WeaponGuid = weapon.Guid; await _session.NotifyAsync(CmdType.StoreItemChangeNotify, new StoreItemChangeNotify { StoreType = StoreType.StorePack, ItemList = { weapon.AsItem() } }); await _session.NotifyAsync(CmdType.AvatarEquipChangeNotify, new AvatarEquipChangeNotify { AvatarGuid = avatar.Guid, EquipType = 6, ItemId = weapon.ItemId, EquipGuid = weapon.Guid, Weapon = weapon.AsSceneWeapon(avatar.WeaponEntityId) }); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"equipped weapon {itemId} lv{weapon.Level} promote {weapon.PromoteLevel}" }; } GameReliquary? reliquary = _player.FindReliquaryByItemId(itemId); if (reliquary is null) return new List { $"item {itemId} is neither a known weapon nor a reliquary" }; reliquary.Level = Math.Max(level, 1u); avatar.ReliquaryGuids[reliquary.EquipType] = reliquary.Guid; await _session.NotifyAsync(CmdType.AvatarEquipChangeNotify, new AvatarEquipChangeNotify { AvatarGuid = avatar.Guid, EquipType = reliquary.EquipType, ItemId = reliquary.ItemId, EquipGuid = reliquary.Guid, Reliquary = reliquary.AsSceneReliquary() }); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"equipped reliquary {itemId} lv{reliquary.Level}" }; } private async ValueTask> KillAsync(string[] args) { if (args.Length >= 1 && args[0].Equals("monster", StringComparison.OrdinalIgnoreCase)) return await ClearAsync(); return new List { "usage: kill monster all" }; } private async ValueTask> TeleportAsync(string[] args) { if (args.Length < 3 || !float.TryParse(args[0], out float x) || !float.TryParse(args[1], out float y) || !float.TryParse(args[2], out float z)) return new List { "usage: /tp x y z" }; await _scene.TeleportAsync(new Vector { X = x, Y = y, Z = z }); return new List { $"teleported to ({x}, {y}, {z})" }; } private async ValueTask> SpawnAsync(string[] args) { if (args.Length < 1 || !uint.TryParse(args[0], out uint monsterId)) return new List { "usage: /spawn monsterId [count]" }; uint count = args.Length > 1 && uint.TryParse(args[1], out uint parsed) ? Math.Min(parsed, 50u) : 1u; uint spawned = 0; for (uint i = 0; i < count; i++) { double angle = Math.PI * 2 * i / Math.Max(count, 1u); Vector pos = new() { X = _state.CurrentPos.X + (float)(Math.Cos(angle) * 4), Y = _state.CurrentPos.Y, Z = _state.CurrentPos.Z + (float)(Math.Sin(angle) * 4) }; GameMonster monster = _player.SpawnMonster(monsterId, pos, 30, 1000f, 100f, 500f); await _scene.AppearMonsterAsync(monster); spawned++; } return new List { $"spawned {spawned}x monster {monsterId}" }; } private async ValueTask> ClearAsync() { await _scene.ClearMonstersAsync(); return new List { "cleared all monsters" }; } private async ValueTask> RespawnAsync() { await _scene.ClearMonstersAsync(); await _worldSpawner.RespawnAsync(_state.CurrentPos); await _persistence.SaveAsync(); return new List { "respawned all monsters around you" }; } private async ValueTask> GiveAsync(string[] args) { if (args.Length < 1 || !uint.TryParse(args[0], out uint itemId)) return new List { "usage: /give itemId [count]" }; uint count = args.Length > 1 && uint.TryParse(args[1], out uint parsed) ? Math.Max(parsed, 1u) : 1u; MaterialExcel? material = _excel.GetTable(ExcelType.Material).Items.OfType() .FirstOrDefault(candidate => candidate.Id == itemId); if (material is not null && material.GainedAvatarId != 0) return await GrantAvatarAsync(material.GainedAvatarId, itemId); bool isWeapon = _excel.GetTable(ExcelType.Weapon).Items.OfType().Any(weapon => weapon.Id == itemId); StoreItemChangeNotify notify = new() { StoreType = StoreType.StorePack }; if (isWeapon) { for (uint i = 0; i < Math.Min(count, 50u); i++) notify.ItemList.Add(_player.CreateWeapon(itemId).AsItem()); } else { _player.GiveMaterial(itemId, count); notify.ItemList.Add(new Item { ItemId = itemId, Guid = _player.MaterialGuid(itemId), Material = new Material { Count = _player.Materials[itemId] } }); } await _session.NotifyAsync(CmdType.StoreItemChangeNotify, notify); ItemAddHintNotify hint = new() { Reason = (uint)ActionReasonType.ActionReasonTrifle }; hint.ItemList.Add(new ItemHint { ItemId = itemId, Count = count, IsNew = false }); await _session.NotifyAsync(CmdType.ItemAddHintNotify, hint); await _persistence.SaveAsync(); return new List { $"gave {count}x {itemId}" }; } private async ValueTask> GrantAvatarAsync(uint avatarId, uint cardItemId) { if (_player.Avatars.Any(avatar => avatar.AvatarId == avatarId)) return new List { $"{cardItemId} is the card for avatar {avatarId}, already owned" }; if (!_player.HasAvatarData(avatarId)) return new List { $"avatar {avatarId} is not in this build" }; GameAvatar? granted = _player.GrantAvatar(avatarId); if (granted is null) return new List { $"avatar {avatarId} could not be created" }; await _session.NotifyAsync(CmdType.AvatarAddNotify, new AvatarAddNotify { Avatar = granted.AsAvatarInfo(), IsInTeam = false }); await _scene.RefreshAvatarDataAsync(); await _persistence.SaveAsync(); List reply = new() { $"granted avatar {avatarId} - add it with: team add {avatarId}" }; if (!_player.HasAbilities(avatarId)) reply.Add("warning: no ability data, it will have no skills and may break the UI - use 'team reset' to recover"); return reply; } private async ValueTask> HealAsync() { await _scene.HealTeamAsync(); return new List { "team healed" }; } private async ValueTask> LevelAsync(string[] args) { GameAvatar? avatar = _player.ActiveAvatar; if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint level)) return new List { "usage: /level n" }; avatar.Level = Math.Clamp(level, 1u, 90u); await _session.NotifyAsync(CmdType.AvatarPropNotify, new AvatarPropNotify { AvatarGuid = avatar.Guid, PropMap = { { 4001u, avatar.Level } } }); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"avatar {avatar.AvatarId} level = {avatar.Level}" }; } private async ValueTask> AscendAsync(string[] args) { GameAvatar? avatar = _player.ActiveAvatar; if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint promote)) return new List { "usage: /asc n" }; avatar.PromoteLevel = Math.Clamp(promote, 0u, 6u); await _session.NotifyAsync(CmdType.AvatarPropNotify, new AvatarPropNotify { AvatarGuid = avatar.Guid, PropMap = { { 1002u, avatar.PromoteLevel } } }); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"avatar {avatar.AvatarId} ascension = {avatar.PromoteLevel}" }; } private async ValueTask> TalentAsync(string[] args) { if (args.Length >= 1 && args[0].Equals("unlock", StringComparison.OrdinalIgnoreCase)) return await ConstellationAsync(new[] { MaxConstellation.ToString() }); GameAvatar? avatar = _player.ActiveAvatar; if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint talent)) return new List { "usage: /talent n, or talent unlock all" }; avatar.TalentLevel = Math.Clamp(talent, 1u, 15u); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"avatar {avatar.AvatarId} talents = {avatar.TalentLevel} (reopen character screen to see)" }; } private async ValueTask> ConstellationAsync(string[] args) { GameAvatar? avatar = _player.ActiveAvatar; if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint constellation)) return new List { "usage: /const n" }; avatar.Constellation = Math.Clamp(constellation, 0u, 6u); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"avatar {avatar.AvatarId} constellation = {avatar.Constellation} (reopen character screen to see)" }; } private async ValueTask> WeaponLevelAsync(string[] args) { GameAvatar? avatar = _player.ActiveAvatar; GameWeapon? weapon = avatar is null ? null : _player.FindWeapon(avatar.WeaponGuid); if (weapon is null || args.Length < 1 || !uint.TryParse(args[0], out uint level)) return new List { "usage: /wlevel n" }; weapon.Level = Math.Clamp(level, 1u, 90u); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"weapon {weapon.ItemId} level = {weapon.Level}" }; } private async ValueTask> RefineAsync(string[] args) { GameAvatar? avatar = _player.ActiveAvatar; GameWeapon? weapon = avatar is null ? null : _player.FindWeapon(avatar.WeaponGuid); if (weapon is null || args.Length < 1 || !uint.TryParse(args[0], out uint refine)) return new List { "usage: /refine n" }; weapon.Refinement = Math.Clamp(refine, 0u, 4u); await _scene.RefreshTeamAsync(); await _persistence.SaveAsync(); return new List { $"weapon {weapon.ItemId} refinement = {weapon.Refinement + 1}" }; } private async ValueTask> WishAsync(string[] args) { uint amount = args.Length > 0 && uint.TryParse(args[0], out uint parsed) ? Math.Clamp(parsed, 1u, 9999u) : 10u; HashSet costItems = new(); foreach (GachaBanner banner in _banners.Banners) { if (banner.Info.CostItemId != 0) costItems.Add(banner.Info.CostItemId); if (banner.Info.TenCostItemId != 0) costItems.Add(banner.Info.TenCostItemId); } if (costItems.Count == 0) return new List { "no banners are shipped, nothing to grant" }; StoreItemChangeNotify notify = new() { StoreType = StoreType.StorePack }; List results = new(); foreach (uint itemId in costItems.OrderBy(id => id)) { _player.GiveMaterial(itemId, amount); uint total = _player.Materials.GetValueOrDefault(itemId); notify.ItemList.Add(new Item { ItemId = itemId, Guid = _player.MaterialGuid(itemId), Material = new Material { Count = total } }); results.Add($"+{amount} x {itemId} (now {total})"); } await _session.NotifyAsync(CmdType.StoreItemChangeNotify, notify); await _persistence.SaveAsync(); return new List { "granted wish currency - open the wish page and pull for the real animation" } .Concat(results).ToList(); } }