feat(runtime): Campaign CC slice CC3 — RuntimeCharacterCreationState

Ports retail's CharGenState as the one Runtime-owned character-creation
state machine, mirroring RuntimeCharacterSelectionState's exact pattern
(snapshot/delta/event-stream, borrow-only view, generation-gated
commands, one mutable owner, no App types). Every command ports a named
retail function: SetHeritageGroup, SetGender, SetTemplate/ApplyTemplate
(Custom = template 0, Olthoi force-lock), the six attribute setters plus
GetAbsRemainingCredits/BalanceAttributes (retail's literal round-robin
order and fairness cursor), SetSkillLevel plus ResetSkillLevels' free-skill
baseline (reusing CC1's ChargenSkillCreditMath two-tier cost lookup
verbatim), RandomizeStartArea, and DoFinish's complete gate sequence
(empty name / unspent attribute credits / already-Pending / client-side
roster-vs-slotCount cap).

LiveSessionController gained a sibling IRuntimeCharacterCreationCommands
implementation, a CreateCharacter wire hook, and a response handler that
reuses existing machinery rather than inventing new paths: the Ok
identity is appended to the roster via RuntimeCharacterSelectionState's
own ApplyRoster, and the "log straight in" behavior reuses the private
EnterSelectedCore. ILiveSessionLifecycleHost gained two default-no-op
hooks (ApplyCharacterCreated/ApplyCreationFailed) so AcDream.App needs
zero changes to keep compiling; wiring them to the status stream is a
CC4 follow-up.

Filed four divergence-register rows for the corners deliberately not
ported: the FPU-unrecoverable FitTemplateToCharacter auto-detect (AP-207,
ACE only reads the field for title text), the per-style color-count
approximation (AP-208, CC1's model has no per-style palette data), the
classID DAT-DID placeholder (AP-209, ACE ignores the field), and
ApplyTemplate's atomic-vs-sequential attribute apply (AP-210).

34 new tests: full state-machine coverage (every Finish gate, every
rejection-code mapping, duplicate-NameInUse tolerance, Olthoi lock,
attribute balance/lock interaction, uncostable-skill rejection) plus a
LiveSessionController integration suite proving the wire send is exactly
55 skill slots (decoded from a real WorldSession + GameMessageCapture)
and the full Ok/rejection round trip through WorldSession.ProcessDatagram.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-15 14:26:10 +02:00
parent f3ef7baae2
commit 9a84230c4f
9 changed files with 3043 additions and 5 deletions

View file

@ -378,6 +378,82 @@ public interface IRuntimeAllegianceCommands
bool on);
}
// ── Character creation (Campaign CC slice CC3, 2026-08-15) ─────────────────
/// <summary>
/// Generation-gated character-creation (CharGenState) outbound actions —
/// lands beside <see cref="IGameRuntimeCommands.CharacterSelection"/>, the
/// same command family shape.
/// </summary>
public interface IRuntimeCharacterCreationCommands
{
RuntimeCommandResult SelectHeritage(
RuntimeGenerationToken expectedGeneration,
uint heritageId);
RuntimeCommandResult SelectGender(
RuntimeGenerationToken expectedGeneration,
uint genderKey);
RuntimeCommandResult SelectTemplate(
RuntimeGenerationToken expectedGeneration,
uint templateIndex);
RuntimeCommandResult SetAttribute(
RuntimeGenerationToken expectedGeneration,
Session.ChargenAttributeId attributeId,
int value);
RuntimeCommandResult SetAttributeLock(
RuntimeGenerationToken expectedGeneration,
Session.ChargenAttributeId attributeId,
bool locked);
RuntimeCommandResult TrainSkill(
RuntimeGenerationToken expectedGeneration,
uint skillId);
RuntimeCommandResult SpecializeSkill(
RuntimeGenerationToken expectedGeneration,
uint skillId);
RuntimeCommandResult UntrainSkill(
RuntimeGenerationToken expectedGeneration,
uint skillId);
RuntimeCommandResult SetAppearanceIndex(
RuntimeGenerationToken expectedGeneration,
Session.ChargenAppearanceSlot slot,
uint index);
RuntimeCommandResult SetShade(
RuntimeGenerationToken expectedGeneration,
Session.ChargenShadeSlot slot,
double value);
RuntimeCommandResult SelectStartArea(
RuntimeGenerationToken expectedGeneration,
int startAreaIndex);
RuntimeCommandResult SetName(
RuntimeGenerationToken expectedGeneration,
string name);
RuntimeCommandResult SetSlot(
RuntimeGenerationToken expectedGeneration,
uint slot);
/// <summary>Retail's Finish button (<c>gmCharGenMainUI::DoFinish @
/// 0x004E9170</c>). On acceptance the request is already on the wire;
/// the Ok/rejection reply arrives asynchronously as a status delta —
/// see <see cref="Session.RuntimeCharacterCreationState"/>.</summary>
RuntimeCommandResult Finish(
RuntimeGenerationToken expectedGeneration);
RuntimeCommandResult AcknowledgeRejection(
RuntimeGenerationToken expectedGeneration);
}
public interface IGameRuntimeCommands
{
IRuntimeSessionCommands Session { get; }
@ -386,6 +462,10 @@ public interface IGameRuntimeCommands
throw new NotSupportedException(
"This command adapter does not project character selection.");
IRuntimeCharacterCreationCommands CharacterCreation =>
throw new NotSupportedException(
"This command adapter does not project character creation.");
IRuntimeSelectionCommands Selection { get; }
IRuntimeCombatCommands Combat { get; }

View file

@ -74,6 +74,8 @@ public sealed class DirectGameRuntimeCommandAdapter
public IRuntimeSessionCommands Session => this;
public IRuntimeCharacterSelectionCommands CharacterSelection =>
_runtime.Session;
public IRuntimeCharacterCreationCommands CharacterCreation =>
_runtime.Session;
public IRuntimeSelectionCommands Selection => this;
public IRuntimeCombatCommands Combat => this;
public IRuntimeMagicCommands Magic => this;

View file

@ -1,5 +1,6 @@
using System.Net;
using System.Net.Sockets;
using AcDream.Core.CharGen;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
@ -107,6 +108,22 @@ public interface ILiveSessionLifecycleHost
void ApplySelectedCharacter(LiveSessionCharacterSelection selection);
void ApplyEnteredWorld(LiveSessionCharacterSelection selection);
void DetachSession(WorldSession session);
/// <summary>
/// Campaign CC slice CC3: reported once per successful character
/// creation (the <c>0xF643</c> Ok identity), right before the roster is
/// re-reported with the new entry appended and the reused enter-selected
/// path runs — the same spot <see cref="ReportRoster"/> and
/// <see cref="ApplyEnteredWorld"/> already occupy. Default no-op: a host
/// that wants status-stream parity with
/// <c>SessionStatusWriter.CharacterCreated</c> overrides this.
/// </summary>
void ApplyCharacterCreated(RuntimeCharacterCreationIdentity identity) { }
/// <summary>Campaign CC slice CC3: reported once per non-Ok <c>0xF643</c>
/// creation response. Default no-op — see <see cref="ApplyCharacterCreated"/>.
/// </summary>
void ApplyCreationFailed(RuntimeCharacterCreationRejection rejection) { }
}
/// <summary>
@ -192,6 +209,15 @@ public interface ILiveSessionOperations
session.SendDeleteCharacter(accountName, activeCharacterIndex);
void RestoreCharacter(WorldSession session, uint characterId) =>
session.SendRestoreCharacter(characterId);
/// <summary>Campaign CC slice CC3: retail's <c>Proto_UI::SendCharGenResult
/// @ 0x00546A70</c> outbound send, reached from <c>gmCharGenMainUI::DoFinish</c>.
/// </summary>
void CreateCharacter(
WorldSession session,
string accountName,
CharacterCreate.Request request,
ReadOnlySpan<uint> skillAdvancementClasses) =>
session.SendCharacterCreation(accountName, request, skillAdvancementClasses);
void Tick(WorldSession session);
void DisposeSession(WorldSession session);
}
@ -247,7 +273,8 @@ internal sealed class ProductionLiveSessionOperations : ILiveSessionOperations
public sealed class LiveSessionController
: IDisposable,
IRuntimeLiveSessionFramePhase,
IRuntimeCharacterSelectionCommands
IRuntimeCharacterSelectionCommands,
IRuntimeCharacterCreationCommands
{
private sealed class CharacterSelectionWireBinding : IDisposable
{
@ -257,6 +284,7 @@ public sealed class LiveSessionController
private readonly Action<CharacterRestore.Parsed> _restore;
private readonly Action<CharacterError.Parsed> _error;
private readonly Action<ServerName.Parsed> _worldName;
private readonly Action<CharGenVerificationResponse.Parsed> _created;
public CharacterSelectionWireBinding(
WorldSession session,
@ -264,7 +292,8 @@ public sealed class LiveSessionController
Action delete,
Action<CharacterRestore.Parsed> restore,
Action<CharacterError.Parsed> error,
Action<ServerName.Parsed> worldName)
Action<ServerName.Parsed> worldName,
Action<CharGenVerificationResponse.Parsed> created)
{
_session = session;
_roster = roster;
@ -272,11 +301,13 @@ public sealed class LiveSessionController
_restore = restore;
_error = error;
_worldName = worldName;
_created = created;
session.CharacterListReceived += roster;
session.CharacterDeleteAcknowledged += delete;
session.CharacterRestoreReceived += restore;
session.CharacterErrorReceived += error;
session.ServerNameReceived += worldName;
session.CharacterCreateResponseReceived += created;
}
public bool IsDisposed => _session is null;
@ -291,6 +322,7 @@ public sealed class LiveSessionController
session.CharacterRestoreReceived -= _restore;
session.CharacterErrorReceived -= _error;
session.ServerNameReceived -= _worldName;
session.CharacterCreateResponseReceived -= _created;
}
}
@ -397,11 +429,19 @@ public sealed class LiveSessionController
public LiveSessionController(
ILiveSessionOperations operations,
TimeProvider? timeProvider = null)
TimeProvider? timeProvider = null,
ChargenOptions? chargenOptions = null)
{
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
CharacterSelectionState = new RuntimeCharacterSelectionState(
timeProvider);
// Campaign CC slice CC3: defaults to ChargenOptions.Empty (no
// heritages configured) — loading the installed DAT's chargen table
// and threading it through is a future App/GameRuntime wiring slice,
// not this one. A caller that never supplies real options simply
// gets an inert chargen surface (every heritage lookup misses).
CharacterCreationState = new RuntimeCharacterCreationState(
chargenOptions ?? ChargenOptions.Empty);
}
public RuntimeCharacterSelectionState CharacterSelectionState { get; }
@ -409,6 +449,11 @@ public sealed class LiveSessionController
public IRuntimeCharacterSelectionView CharacterSelection =>
CharacterSelectionState.View;
public RuntimeCharacterCreationState CharacterCreationState { get; }
public IRuntimeCharacterCreationView CharacterCreation =>
CharacterCreationState.View;
public WorldSession? CurrentSession
{
get { lock (_gate) return _scope?.Session; }
@ -684,6 +729,7 @@ public sealed class LiveSessionController
ulong generation = ++_generation;
RuntimeGenerationToken activeGeneration = new(generation);
CharacterSelectionState.Reset(activeGeneration);
CharacterCreationState.Reset(activeGeneration);
try
{
DrainRetiredScope();
@ -710,6 +756,7 @@ public sealed class LiveSessionController
return new LiveSessionStartResult(LiveSessionStartStatus.MissingCredentials);
CharacterSelectionState.Begin(activeGeneration);
CharacterCreationState.Begin(activeGeneration);
SessionScope? scope = null;
try
@ -921,6 +968,14 @@ public sealed class LiveSessionController
if (IsCurrent(scope, generation))
CharacterSelectionState.ApplyWorldName(worldName.WorldName);
}
},
created =>
{
lock (_gate)
{
if (IsCurrent(scope, generation))
HandleCharacterCreationResponse(scope, generation, created);
}
});
public RuntimeCommandResult Highlight(
@ -1149,6 +1204,350 @@ public sealed class LiveSessionController
new RuntimeGenerationToken(_generation),
characterId);
// ── Character creation (Campaign CC slice CC3) ─────────────────────
/// <summary>Collects <see cref="RuntimeCharacterSelectionEntry"/> rows
/// into <see cref="LiveSessionRosterEntry"/> form for
/// <see cref="HandleCharacterCreationResponse"/>'s roster-append
/// composition.</summary>
private sealed class RosterCollector(List<LiveSessionRosterEntry> entries)
: IRuntimeCharacterSelectionVisitor
{
public void Visit(in RuntimeCharacterSelectionEntry character) =>
entries.Add(new LiveSessionRosterEntry(
character.CharacterId,
character.Name,
character.SecondsGreyedOut));
}
/// <summary>
/// Ports the Ok half of <c>Handle_CharGenVerificationResponse @
/// 0x0055E8B0</c> case 1 (the PENDING/create branch —
/// <c>CharacterSet::AddIdentity</c>) composed with
/// <c>gmCharGenMainUI::Update @ 0x004E8460</c>'s per-frame roster scan
/// (which finds the freshly appended identity by name and calls
/// <c>CPlayerSystem::LogOnCharacter</c> directly): a fresh
/// <see cref="RuntimeCharacterCreationState"/> is disambiguated locally
/// (we already have the exact created guid/name from the SAME reply
/// that triggered the roster append, so there is no need to re-scan for
/// it the way retail's per-frame poll does — an equivalent, not a
/// divergent, substitution). Reuses <see cref="RuntimeCharacterSelectionState.ApplyRoster"/>
/// for the append (there is no single-entry append primitive to
/// duplicate) and <see cref="EnterSelectedCore"/> for the log-straight-in
/// (no second enter route). A non-Ok reply only needs the state-machine
/// update already performed by <see cref="RuntimeCharacterCreationState.ApplyCreationResponse"/>
/// — no roster/enter side effects.
/// </summary>
private void HandleCharacterCreationResponse(
SessionScope scope,
ulong generation,
CharGenVerificationResponse.Parsed response)
{
CharacterCreationState.ApplyCreationResponse(response);
RuntimeCharacterCreationSnapshot creation = CharacterCreationState.Snapshot;
if (creation.LastCreated is { } created)
scope.Host.ApplyCharacterCreated(created);
else if (creation.LastRejection is { } rejection)
scope.Host.ApplyCreationFailed(rejection);
if (creation.LastCreated is not { } identity)
return;
RuntimeCharacterSelectionSnapshot before = CharacterSelectionState.Snapshot;
var entries = new List<LiveSessionRosterEntry>(before.RosterCount + 1);
CharacterSelectionState.View.Visit(new RosterCollector(entries));
entries.Add(new LiveSessionRosterEntry(
identity.Guid,
identity.Name,
SecondsGreyedOut: 0u));
var report = new LiveSessionRosterReport(
before.AccountName,
before.SlotCount,
entries);
CharacterSelectionState.ApplyRoster(report);
scope.Host.ReportRoster(report);
if (!IsCurrent(scope, generation))
return;
if (!CharacterSelectionState.TryHighlight(identity.Guid))
return;
if (!IsCurrent(scope, generation))
return;
// Inline, not through the public Enter() command: we are already
// running inside Tick()'s top-level operation (this handler fires
// synchronously from _operations.Tick's inbound processing), exactly
// the same calling convention StartCore's own inline enter uses.
_ = EnterSelectedCore();
}
public RuntimeCommandResult SelectHeritage(
RuntimeGenerationToken expectedGeneration,
uint heritageId)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TrySelectHeritage(heritageId));
}
}
public RuntimeCommandResult SelectGender(
RuntimeGenerationToken expectedGeneration,
uint genderKey)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TrySelectGender(genderKey));
}
}
public RuntimeCommandResult SelectTemplate(
RuntimeGenerationToken expectedGeneration,
uint templateIndex)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TrySelectTemplate(templateIndex));
}
}
public RuntimeCommandResult SetAttribute(
RuntimeGenerationToken expectedGeneration,
ChargenAttributeId attributeId,
int value)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TrySetAttribute(attributeId, value));
}
}
public RuntimeCommandResult SetAttributeLock(
RuntimeGenerationToken expectedGeneration,
ChargenAttributeId attributeId,
bool locked)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TrySetAttributeLock(attributeId, locked));
}
}
public RuntimeCommandResult TrainSkill(
RuntimeGenerationToken expectedGeneration,
uint skillId)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(CharacterCreationState.TryTrainSkill(skillId));
}
}
public RuntimeCommandResult SpecializeSkill(
RuntimeGenerationToken expectedGeneration,
uint skillId)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(CharacterCreationState.TrySpecializeSkill(skillId));
}
}
public RuntimeCommandResult UntrainSkill(
RuntimeGenerationToken expectedGeneration,
uint skillId)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(CharacterCreationState.TryUntrainSkill(skillId));
}
}
public RuntimeCommandResult SetAppearanceIndex(
RuntimeGenerationToken expectedGeneration,
ChargenAppearanceSlot slot,
uint index)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TrySetAppearanceIndex(slot, index));
}
}
public RuntimeCommandResult SetShade(
RuntimeGenerationToken expectedGeneration,
ChargenShadeSlot slot,
double value)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(CharacterCreationState.TrySetShade(slot, value));
}
}
public RuntimeCommandResult SelectStartArea(
RuntimeGenerationToken expectedGeneration,
int startAreaIndex)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TrySelectStartArea(startAreaIndex));
}
}
public RuntimeCommandResult SetName(
RuntimeGenerationToken expectedGeneration,
string name)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(CharacterCreationState.TrySetName(name ?? string.Empty));
}
}
public RuntimeCommandResult SetSlot(
RuntimeGenerationToken expectedGeneration,
uint slot)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(CharacterCreationState.TrySetSlot(slot));
}
}
/// <summary>
/// Ports <c>gmCharGenMainUI::DoFinish @ 0x004E9170</c>'s send half: the
/// local gates live in <see cref="RuntimeCharacterCreationState.TryBeginFinish"/>;
/// this method supplies the roster/slot-cap inputs from
/// <see cref="CharacterSelectionState"/> and, on acceptance, sends the
/// wire request via <c>Proto_UI::SendCharGenResult</c>'s port
/// (<see cref="ILiveSessionOperations.CreateCharacter"/>). A transport
/// failure resets the verification latch the same way an unsolicited
/// Undef/Pending reply does (<see cref="RuntimeCharacterCreationState.ApplyCreationResponse"/>)
/// rather than leaving it stuck Pending forever.
/// </summary>
public RuntimeCommandResult Finish(RuntimeGenerationToken expectedGeneration)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
RuntimeCharacterSelectionSnapshot selection = CharacterSelectionState.Snapshot;
if (!CharacterCreationState.TryBeginFinish(
selection.RosterCount,
selection.SlotCount,
out CharacterCreate.Request request,
out uint[] skillAdvancementClasses,
out _))
{
return CharacterCreationResult(RuntimeCommandStatus.Rejected);
}
try
{
_operations.CreateCharacter(
_scope!.Session,
selection.AccountName,
request,
skillAdvancementClasses);
return CharacterCreationResult(RuntimeCommandStatus.Accepted);
}
catch
{
CharacterCreationState.ApplyCreationResponse(
new CharGenVerificationResponse.Parsed(
(uint)CharGenVerificationResponse.Code.Undef,
null,
null,
null));
return CharacterCreationResult(RuntimeCommandStatus.Rejected);
}
}
}
public RuntimeCommandResult AcknowledgeRejection(
RuntimeGenerationToken expectedGeneration)
{
lock (_gate)
{
RuntimeCommandStatus gate = ValidateCharacterCreationCommand(expectedGeneration);
if (gate != RuntimeCommandStatus.Accepted)
return CharacterCreationResult(gate);
return CharacterCreationResult(
CharacterCreationState.TryAcknowledgeRejection());
}
}
private RuntimeCommandStatus ValidateCharacterCreationCommand(
RuntimeGenerationToken expectedGeneration)
{
RuntimeGenerationToken current = new(_generation);
if (expectedGeneration != current)
return RuntimeCommandStatus.StaleGeneration;
if (_disposed || _disposeRequested || _scope is null || _inWorld)
return RuntimeCommandStatus.Inactive;
return CharacterSelectionState.Snapshot.Lifecycle
== RuntimeCharacterSelectionLifecycle.AwaitingSelection
? RuntimeCommandStatus.Accepted
: RuntimeCommandStatus.Inactive;
}
private RuntimeCommandResult CharacterCreationResult(bool accepted) =>
CharacterCreationResult(
accepted ? RuntimeCommandStatus.Accepted : RuntimeCommandStatus.Rejected);
private RuntimeCommandResult CharacterCreationResult(RuntimeCommandStatus status) =>
new(status, new RuntimeGenerationToken(_generation));
private void StopCore()
{
// MUST-FIX 1: TS-71's logout-flush half — retail's
@ -1165,6 +1564,7 @@ public sealed class LiveSessionController
_inWorld = false;
_activeSelection = null;
CharacterSelectionState.Reset(new RuntimeGenerationToken(_generation));
CharacterCreationState.Reset(new RuntimeGenerationToken(_generation));
if (_scope is { } scope)
{
_scope = null;
@ -1306,6 +1706,7 @@ public sealed class LiveSessionController
{
StopCore();
CharacterSelectionState.Dispose();
CharacterCreationState.Dispose();
_disposed = true;
}

File diff suppressed because it is too large Load diff