yuanshen/Yuanshen.Gameserver/Game/GmCommandService.cs
wisedev a4b3b669a8 Use the named retcode and reason constants
Half the handlers were still answering with a bare 0 or -1 even though the whole
retcode table is generated. Values on the wire are unchanged, they just read
properly now.

Same for the item hint reasons. ActionReasonType is a generated enum and we were
redeclaring six of its members as private consts in four different files.
2026-09-04 11:21:12 +03:00

591 lines
23 KiB
C#

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<GmCommandService> logger)
{
_player = player;
_state = state;
_scene = scene;
_session = session;
_persistence = persistence;
_excel = excel;
_worldSpawner = worldSpawner;
_banners = banners;
_logger = logger;
}
public async ValueTask<IReadOnlyList<string>> 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<string> 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<string> { $"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<string> { $"/{command} failed: {exception.Message}" };
}
}
private static List<string> 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<List<string>> AvatarAsync(string[] args)
{
if (args.Length >= 2 && args[0].Equals("add", StringComparison.OrdinalIgnoreCase))
return await TeamAddAsync(args[1]);
return new List<string> { "usage: avatar add avatarId" };
}
private async ValueTask<List<string>> 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<string> { "usage: team add|remove avatarId, team list, team reset" };
return action switch
{
"add" => await TeamAddAsync(args[1]),
"remove" => await TeamRemoveAsync(args[1]),
_ => new List<string> { "usage: team add|remove avatarId, team list, team reset" }
};
}
private List<string> TeamList()
=> _player.TeamAvatars
.Select(avatar => $"{avatar.AvatarId} guid={avatar.Guid}")
.DefaultIfEmpty("team is empty")
.ToList();
private async ValueTask<List<string>> TeamAddAsync(string rawId)
{
if (!uint.TryParse(rawId, out uint avatarId))
return new List<string> { "usage: team add avatarId" };
GameAvatar? avatar = _player.Avatars.FirstOrDefault(candidate => candidate.AvatarId == avatarId);
if (avatar is null)
{
List<string> 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<string> { $"avatar {avatarId} is already in the team" };
if (_player.TeamAvatarGuids.Count >= TeamSize)
return new List<string> { $"team is full, remove one first (max {TeamSize})" };
List<ulong> team = _player.TeamAvatarGuids.Append(avatar.Guid).ToList();
await _scene.SetupTeamAsync(_player.CurrentTeamId, team, _player.CurrentAvatarGuid);
await _persistence.SaveAsync();
return new List<string> { $"added avatar {avatarId} to the team" };
}
private async ValueTask<List<string>> TeamResetAsync()
{
_player.ResetTeam();
await _scene.SetupTeamAsync(_player.CurrentTeamId, _player.TeamAvatarGuids.ToList(), _player.CurrentAvatarGuid);
await _persistence.SaveAsync();
return new List<string> { "team reset to the default avatar" };
}
private async ValueTask<List<string>> TeamRemoveAsync(string rawId)
{
if (!uint.TryParse(rawId, out uint avatarId))
return new List<string> { "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<string> { $"avatar {avatarId} is not in the team" };
if (_player.TeamAvatarGuids.Count <= 1)
return new List<string> { "cannot remove the last team member" };
List<ulong> 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<string> { $"removed avatar {avatarId} from the team" };
}
private static List<string> Unsupported(string verb)
=> new() { $"{verb} is not implemented on this server yet" };
private async ValueTask<List<string>> ItemAsync(string[] args)
{
if (args.Length >= 2 && args[0].Equals("add", StringComparison.OrdinalIgnoreCase))
return await GiveAsync(args[1..]);
return new List<string> { "usage: item add itemId [count]" };
}
private async ValueTask<List<string>> ScoinAsync(string[] args)
{
if (args.Length < 1 || !uint.TryParse(args[0], out uint amount))
return new List<string> { "usage: scoin amount" };
_player.Scoin += amount;
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
await _persistence.SaveAsync();
return new List<string> { $"mora = {_player.Scoin}" };
}
private async ValueTask<List<string>> HcoinAsync(string[] args)
{
if (args.Length < 1 || !uint.TryParse(args[0], out uint amount))
return new List<string> { "usage: hcoin amount" };
_player.Hcoin += amount;
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
await _persistence.SaveAsync();
return new List<string> { $"primogems = {_player.Hcoin}" };
}
private async ValueTask<List<string>> PlayerAsync(string[] args)
{
if (args.Length < 2 || !args[0].Equals("level", StringComparison.OrdinalIgnoreCase)
|| !uint.TryParse(args[1], out uint level))
{
return new List<string> { "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<string> { $"player level = {level} (display only, not persisted)" };
}
private async ValueTask<List<string>> SkillAsync(string[] args)
{
if (args.Length < 2 || !uint.TryParse(args[1], out uint level))
return new List<string> { "usage: skill slot level" };
return await TalentAsync(new[] { level.ToString() });
}
private async ValueTask<List<string>> EquipAsync(string[] args)
{
if (args.Length < 2 || !args[0].Equals("add", StringComparison.OrdinalIgnoreCase)
|| !uint.TryParse(args[1], out uint itemId))
{
return new List<string> { "usage: equip add itemId [level] [promote]" };
}
GameAvatar? avatar = _player.ActiveAvatar;
if (avatar is null)
return new List<string> { "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<WeaponExcel>().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<string> { $"equipped weapon {itemId} lv{weapon.Level} promote {weapon.PromoteLevel}" };
}
GameReliquary? reliquary = _player.FindReliquaryByItemId(itemId);
if (reliquary is null)
return new List<string> { $"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<string> { $"equipped reliquary {itemId} lv{reliquary.Level}" };
}
private async ValueTask<List<string>> KillAsync(string[] args)
{
if (args.Length >= 1 && args[0].Equals("monster", StringComparison.OrdinalIgnoreCase))
return await ClearAsync();
return new List<string> { "usage: kill monster all" };
}
private async ValueTask<List<string>> 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<string> { "usage: /tp x y z" };
await _scene.TeleportAsync(new Vector { X = x, Y = y, Z = z });
return new List<string> { $"teleported to ({x}, {y}, {z})" };
}
private async ValueTask<List<string>> SpawnAsync(string[] args)
{
if (args.Length < 1 || !uint.TryParse(args[0], out uint monsterId))
return new List<string> { "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<string> { $"spawned {spawned}x monster {monsterId}" };
}
private async ValueTask<List<string>> ClearAsync()
{
await _scene.ClearMonstersAsync();
return new List<string> { "cleared all monsters" };
}
private async ValueTask<List<string>> RespawnAsync()
{
await _scene.ClearMonstersAsync();
await _worldSpawner.RespawnAsync(_state.CurrentPos);
await _persistence.SaveAsync();
return new List<string> { "respawned all monsters around you" };
}
private async ValueTask<List<string>> GiveAsync(string[] args)
{
if (args.Length < 1 || !uint.TryParse(args[0], out uint itemId))
return new List<string> { "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<MaterialExcel>()
.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<WeaponExcel>().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<string> { $"gave {count}x {itemId}" };
}
private async ValueTask<List<string>> GrantAvatarAsync(uint avatarId, uint cardItemId)
{
if (_player.Avatars.Any(avatar => avatar.AvatarId == avatarId))
return new List<string> { $"{cardItemId} is the card for avatar {avatarId}, already owned" };
if (!_player.HasAvatarData(avatarId))
return new List<string> { $"avatar {avatarId} is not in this build" };
GameAvatar? granted = _player.GrantAvatar(avatarId);
if (granted is null)
return new List<string> { $"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<string> 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<List<string>> HealAsync()
{
await _scene.HealTeamAsync();
return new List<string> { "team healed" };
}
private async ValueTask<List<string>> LevelAsync(string[] args)
{
GameAvatar? avatar = _player.ActiveAvatar;
if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint level))
return new List<string> { "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<string> { $"avatar {avatar.AvatarId} level = {avatar.Level}" };
}
private async ValueTask<List<string>> AscendAsync(string[] args)
{
GameAvatar? avatar = _player.ActiveAvatar;
if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint promote))
return new List<string> { "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<string> { $"avatar {avatar.AvatarId} ascension = {avatar.PromoteLevel}" };
}
private async ValueTask<List<string>> 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<string> { "usage: /talent n, or talent unlock all" };
avatar.TalentLevel = Math.Clamp(talent, 1u, 15u);
await _scene.RefreshTeamAsync();
await _persistence.SaveAsync();
return new List<string> { $"avatar {avatar.AvatarId} talents = {avatar.TalentLevel} (reopen character screen to see)" };
}
private async ValueTask<List<string>> ConstellationAsync(string[] args)
{
GameAvatar? avatar = _player.ActiveAvatar;
if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint constellation))
return new List<string> { "usage: /const n" };
avatar.Constellation = Math.Clamp(constellation, 0u, 6u);
await _scene.RefreshTeamAsync();
await _persistence.SaveAsync();
return new List<string> { $"avatar {avatar.AvatarId} constellation = {avatar.Constellation} (reopen character screen to see)" };
}
private async ValueTask<List<string>> 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<string> { "usage: /wlevel n" };
weapon.Level = Math.Clamp(level, 1u, 90u);
await _scene.RefreshTeamAsync();
await _persistence.SaveAsync();
return new List<string> { $"weapon {weapon.ItemId} level = {weapon.Level}" };
}
private async ValueTask<List<string>> 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<string> { "usage: /refine n" };
weapon.Refinement = Math.Clamp(refine, 0u, 4u);
await _scene.RefreshTeamAsync();
await _persistence.SaveAsync();
return new List<string> { $"weapon {weapon.ItemId} refinement = {weapon.Refinement + 1}" };
}
private async ValueTask<List<string>> WishAsync(string[] args)
{
uint amount = args.Length > 0 && uint.TryParse(args[0], out uint parsed) ? Math.Clamp(parsed, 1u, 9999u) : 10u;
HashSet<uint> 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<string> { "no banners are shipped, nothing to grant" };
StoreItemChangeNotify notify = new() { StoreType = StoreType.StorePack };
List<string> 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<string> { "granted wish currency - open the wish page and pull for the real animation" }
.Concat(results).ToList();
}
}