367 lines
13 KiB
C#
367 lines
13 KiB
C#
using CapySR.GameServer.Game;
|
|
using CapySR.GameServer.Net;
|
|
using CapySR.GameServer.Scene;
|
|
using CapySR.Protocol;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace CapySR.GameServer.Handlers;
|
|
|
|
public static class SessionExtensions
|
|
{
|
|
// base ids of the team walking around, or the first four the account owns
|
|
public static List<uint> DefaultLineup(this PlayerSession session)
|
|
{
|
|
var current = session.Player.Lineups.Current.AvatarIds;
|
|
|
|
if (current.Count > 0)
|
|
{
|
|
return current;
|
|
}
|
|
|
|
return [.. session.World.OwnedAvatarIds.Select(session.World.Data.BaseAvatarId).Distinct().Take(Lineup.Size)];
|
|
}
|
|
|
|
public static SceneInfo CurrentScene(this PlayerSession session)
|
|
{
|
|
if (session.Scene.Info is { } info)
|
|
{
|
|
return info;
|
|
}
|
|
|
|
var scene = session.Challenge.Active
|
|
? session.World.Scenes.BuildChallengeScene(session.Challenge)
|
|
: session.World.Scenes.BuildWorldScene(
|
|
session.Player.EntryId, session.DefaultLineup(), session.Player.TeleportId,
|
|
session.Config.GameServer.WorldId, session.Player.Position);
|
|
|
|
session.Scene.Load(scene, session.Challenge.Active);
|
|
return scene;
|
|
}
|
|
|
|
public static LineupInfo BuildLineupInfo(this PlayerSession session, Lineup lineup, ExtraLineupType type, uint index)
|
|
{
|
|
var book = session.Player.Lineups;
|
|
var isCurrent = ReferenceEquals(lineup, book.Current);
|
|
var maxMp = isCurrent ? book.MaxMp : MaxMpFor(lineup);
|
|
|
|
var planeId = session.Scene.PlaneId != 0
|
|
? session.Scene.PlaneId
|
|
: session.World.Data.ResolveEntrance(session.Player.EntryId)?.Entrance.PlaneID ?? 0;
|
|
|
|
var info = new LineupInfo
|
|
{
|
|
Name = lineup.Name,
|
|
ExtraLineupType = type,
|
|
LeaderSlot = lineup.LeaderSlot,
|
|
Mp = Math.Min(book.Mp, maxMp),
|
|
MaxMp = maxMp,
|
|
PlaneId = planeId,
|
|
Index = type == ExtraLineupType.LineupNone ? index : 0,
|
|
IsVirtual = type != ExtraLineupType.LineupNone,
|
|
};
|
|
|
|
foreach (var (slot, avatarId) in lineup.Occupied)
|
|
{
|
|
var path = Roster.ResolvePath(session.World, session.Player, avatarId);
|
|
var state = session.Player.StateOf(path, session.World.Player);
|
|
|
|
info.AvatarList.Add(new LineupAvatar
|
|
{
|
|
Id = avatarId,
|
|
Slot = slot,
|
|
Hp = state.Hp,
|
|
Satiety = 100,
|
|
AvatarType = AvatarType.AvatarFormalType,
|
|
SpBar = state.SpBar,
|
|
});
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
private static uint MaxMpFor(Lineup lineup) =>
|
|
lineup.AvatarIds.Any(id => id is 1408 or 1510) ? LineupBook.BaseMaxMp + 3 : LineupBook.BaseMaxMp;
|
|
|
|
public static LineupInfo BuildWorldLineup(this PlayerSession session)
|
|
{
|
|
var book = session.Player.Lineups;
|
|
return session.BuildLineupInfo(book.Current, book.ExtraType, book.CurrentIndex);
|
|
}
|
|
|
|
public static LineupInfo BuildChallengeLineup(this PlayerSession session, int node)
|
|
{
|
|
var type = node == 0 ? ExtraLineupType.LineupChallenge : ExtraLineupType.LineupChallenge2;
|
|
var lineup = session.Player.Lineups.Extra(type)
|
|
?? session.Player.Lineups.SetExtra(type, session.Challenge.Lineups[node], activate: false);
|
|
|
|
return session.BuildLineupInfo(lineup, type, 0);
|
|
}
|
|
|
|
// the client only redraws the team once the server confirms it
|
|
public static Task SyncLineupAsync(this PlayerSession session, SyncLineupReason reason = SyncLineupReason.SyncReasonNone)
|
|
{
|
|
var notify = new SyncLineupNotify { Lineup = session.BuildWorldLineup() };
|
|
|
|
if (reason != SyncLineupReason.SyncReasonNone)
|
|
{
|
|
notify.ReasonList.Add(reason);
|
|
}
|
|
|
|
return session.SendAsync(notify);
|
|
}
|
|
|
|
// the overworld models are the scene's actor entities; swap them in place instead of
|
|
// reloading the whole scene when the team changes
|
|
public static async Task RefreshSceneActorsAsync(this PlayerSession session)
|
|
{
|
|
var scene = session.Scene;
|
|
|
|
if (scene.Info is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var data = session.World.Data;
|
|
var lineup = session.Player.Lineups.Current;
|
|
var desired = lineup.AvatarIds.Select(data.BaseAvatarId).Distinct().ToList();
|
|
var existing = scene.Actors.ToList();
|
|
|
|
var removed = existing.Where(a => !desired.Contains(a.AvatarId)).ToList();
|
|
var added = desired.Where(id => existing.All(a => a.AvatarId != id)).ToList();
|
|
|
|
var leaderEntityId = SceneBuilder.ActorEntityId(data.BaseAvatarId(lineup.LeaderAvatarId));
|
|
|
|
if (removed.Count == 0 && added.Count == 0)
|
|
{
|
|
scene.Info.LeaderEntityId = leaderEntityId;
|
|
return;
|
|
}
|
|
|
|
var motion = session.Player.Position?.ToMotion()
|
|
?? existing.FirstOrDefault()?.Motion
|
|
?? session.World.Scenes.SpawnMotion(scene.EntryId, session.Player.TeleportId, null);
|
|
|
|
var actors = desired.Select(id => session.World.Scenes.BuildActor(id, motion)).ToList();
|
|
scene.ReplaceActors(actors, leaderEntityId);
|
|
|
|
var group = new GroupRefreshInfo
|
|
{
|
|
GroupId = 0,
|
|
RefreshType = removed.Count > 0 ? SceneGroupRefreshType.Afibfmafncc : SceneGroupRefreshType.Loaded,
|
|
};
|
|
|
|
foreach (var actor in removed)
|
|
{
|
|
group.RefreshEntity.Add(new SceneEntityRefreshInfo { DeleteEntity = actor.EntityId });
|
|
}
|
|
|
|
foreach (var actor in actors.Where(a => added.Contains(a.Actor.BaseAvatarId)))
|
|
{
|
|
group.RefreshEntity.Add(new SceneEntityRefreshInfo { AddEntity = actor });
|
|
}
|
|
|
|
await session.SendAsync(new SceneGroupRefreshScNotify
|
|
{
|
|
FloorId = scene.FloorId,
|
|
GroupRefreshList = { group },
|
|
});
|
|
}
|
|
|
|
public static async Task RemoveEntitiesAsync(this PlayerSession session, IEnumerable<uint> entityIds)
|
|
{
|
|
var scene = session.Scene;
|
|
var groups = new Dictionary<uint, GroupRefreshInfo>();
|
|
|
|
foreach (var entityId in entityIds.Distinct())
|
|
{
|
|
var entity = scene.Get(entityId);
|
|
|
|
if (entity is null || !scene.Remove(entityId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!groups.TryGetValue(entity.GroupId, out var group))
|
|
{
|
|
groups[entity.GroupId] = group = new GroupRefreshInfo
|
|
{
|
|
GroupId = entity.GroupId,
|
|
RefreshType = SceneGroupRefreshType.Afibfmafncc,
|
|
};
|
|
}
|
|
|
|
group.RefreshEntity.Add(new SceneEntityRefreshInfo { DeleteEntity = entityId });
|
|
}
|
|
|
|
if (groups.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var notify = new SceneGroupRefreshScNotify { FloorId = scene.FloorId };
|
|
notify.GroupRefreshList.AddRange(groups.Values);
|
|
await session.SendAsync(notify);
|
|
}
|
|
|
|
// a real scene change: teleport, map travel, leaving a challenge
|
|
public static async Task EnterWorldSceneAsync(
|
|
this PlayerSession session, uint entryId, uint teleportId, EnterSceneReason reason = EnterSceneReason.None,
|
|
bool keepPosition = false)
|
|
{
|
|
var player = session.Player;
|
|
var world = session.World;
|
|
|
|
session.Challenge.Clear();
|
|
session.Battles.Quit();
|
|
player.Lineups.ClearExtra();
|
|
|
|
if (world.Data.ResolveEntrance(entryId) is null && world.Data.GetScene(entryId) is null)
|
|
{
|
|
session.Logger.LogWarning("entry {Entry} is unknown; staying at {Current}", entryId, player.EntryId);
|
|
entryId = player.EntryId;
|
|
}
|
|
|
|
player.EntryId = entryId;
|
|
player.TeleportId = teleportId;
|
|
|
|
if (!keepPosition)
|
|
{
|
|
player.Position = null;
|
|
}
|
|
|
|
player.Lineups.RefillMp();
|
|
player.HealAll(world.Player);
|
|
player.Touch();
|
|
|
|
var scene = world.Scenes.BuildWorldScene(
|
|
entryId, session.DefaultLineup(), teleportId, session.Config.GameServer.WorldId,
|
|
keepPosition ? player.Position : null);
|
|
|
|
session.Scene.Load(scene, isChallenge: false);
|
|
|
|
await session.SendAsync(new EnterSceneByServerScNotify
|
|
{
|
|
Reason = reason,
|
|
Lineup = session.BuildWorldLineup(),
|
|
Scene = scene,
|
|
});
|
|
|
|
await session.NotifySpawnPositionAsync(scene);
|
|
}
|
|
|
|
public static async Task EnterChallengeSceneAsync(this PlayerSession session)
|
|
{
|
|
var scene = session.World.Scenes.BuildChallengeScene(session.Challenge);
|
|
session.Scene.Load(scene, isChallenge: true);
|
|
|
|
await session.SendAsync(new EnterSceneByServerScNotify
|
|
{
|
|
Reason = EnterSceneReason.None,
|
|
Lineup = session.BuildWorldLineup(),
|
|
Scene = scene,
|
|
});
|
|
|
|
await session.NotifySpawnPositionAsync(scene);
|
|
}
|
|
|
|
// the client places actors where the move notify says, so repeat the spawn for each
|
|
public static async Task NotifySpawnPositionAsync(this PlayerSession session, SceneInfo scene)
|
|
{
|
|
foreach (var group in scene.EntityGroupList)
|
|
{
|
|
foreach (var entity in group.EntityList.Where(e => e.Actor is not null && e.Motion is not null))
|
|
{
|
|
await session.SendAsync(new SceneEntityMoveScNotify
|
|
{
|
|
EntityId = entity.EntityId,
|
|
EntryId = scene.EntryId,
|
|
Motion = entity.Motion,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public static async Task MoveActorsAsync(this PlayerSession session, MotionInfo motion)
|
|
{
|
|
var scene = session.Scene;
|
|
|
|
foreach (var actor in scene.Actors.ToList())
|
|
{
|
|
await session.SendAsync(new SceneEntityMoveScNotify
|
|
{
|
|
EntityId = actor.EntityId,
|
|
EntryId = scene.EntryId,
|
|
Motion = motion,
|
|
});
|
|
}
|
|
|
|
session.Player.Position = PlayerPosition.From(motion);
|
|
}
|
|
|
|
// heal the walking team; the fallen stay down, as in the real thing
|
|
public static void HealParty(this PlayerSession session, uint amount)
|
|
{
|
|
foreach (var avatarId in session.Player.Lineups.Current.AvatarIds)
|
|
{
|
|
var path = Roster.ResolvePath(session.World, session.Player, avatarId);
|
|
var state = session.Player.StateOf(path, session.World.Player);
|
|
|
|
if (state.Hp > 0)
|
|
{
|
|
state.Hp = Math.Min(Player.FullHp, state.Hp + amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static Task SyncAvatarsAsync(this PlayerSession session)
|
|
{
|
|
var sync = new PlayerSyncScNotify { AvatarSync = new AvatarSync() };
|
|
sync.AvatarSync.AvatarList.AddRange(Roster.BuildAvatars(session.World, session.Player));
|
|
sync.AvatarSync.AvatarPathDataInfoList.AddRange(Roster.BuildPathData(session.World, session.Player));
|
|
return session.SendAsync(sync);
|
|
}
|
|
|
|
// a fresh srtools upload: new roster, new gear, teams pruned to what still exists
|
|
public static async Task OnSrToolsReloadedAsync(this PlayerSession session)
|
|
{
|
|
var world = session.World;
|
|
var player = session.Player;
|
|
|
|
session.Logger.LogInformation("srtools data changed; pushing {Avatars} avatars to the client", world.OwnedAvatarIds.Count);
|
|
|
|
var owned = world.OwnedAvatarIds.Select(world.Data.BaseAvatarId).Distinct().ToList();
|
|
player.Lineups.Prune(id => owned.Contains(id));
|
|
player.Lineups.Seed(owned.Take(Lineup.Size));
|
|
|
|
if (player.Lineups.Current.IsEmpty)
|
|
{
|
|
player.Lineups.Squad(player.Lineups.CurrentIndex).Fill(owned.Take(Lineup.Size));
|
|
}
|
|
|
|
player.AvatarStates.Clear();
|
|
player.Touch();
|
|
|
|
var sync = new PlayerSyncScNotify { AvatarSync = new AvatarSync() };
|
|
sync.AvatarSync.AvatarList.AddRange(Roster.BuildAvatars(world, player));
|
|
sync.AvatarSync.AvatarPathDataInfoList.AddRange(Roster.BuildPathData(world, player));
|
|
|
|
foreach (var relic in InventoryHandlers.VisibleRelics(session))
|
|
{
|
|
sync.RelicList.Add(InventoryHandlers.ToProto(relic));
|
|
}
|
|
|
|
foreach (var lightcone in InventoryHandlers.VisibleLightcones(session))
|
|
{
|
|
sync.EquipmentList.Add(InventoryHandlers.ToProto(lightcone));
|
|
}
|
|
|
|
await session.SendAsync(sync);
|
|
await session.SyncLineupAsync();
|
|
|
|
if (!session.Challenge.Active)
|
|
{
|
|
await session.RefreshSceneActorsAsync();
|
|
}
|
|
}
|
|
}
|