Merge branch 'phase/46-speech-home'

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	docs/phases/09-social/README.md
#	src/HSchool.Server/Game/SchoolWorker.cs
#	tests/HSchool.Ai.Tests/TalkCirclesTests.cs
#	tests/HSchool.Simulation.Tests/MorningOpinionsTests.cs
#	tests/HSchool.Simulation.Tests/TalkCircleTests.cs
This commit is contained in:
Leonid Pershin
2026-08-20 14:06:29 +03:00
35 changed files with 910 additions and 36 deletions
+10
View File
@@ -113,4 +113,14 @@ internal abstract record GameCommand
DressRulePair? PendingStudents,
DressRulePair? PendingStaff,
TaskCompletionSource<DressRulesOutcome> Result) : GameCommand;
internal sealed record GetSpeechRules(
int SchoolId,
TaskCompletionSource<SpeechRulesOutcome> Result) : GameCommand;
internal sealed record SetSpeechRules(
int SchoolId,
string? PendingStudents,
string? PendingStaff,
TaskCompletionSource<SpeechRulesOutcome> Result) : GameCommand;
}
@@ -290,6 +290,17 @@ internal sealed class GameLoopService(
new WorkerCommand.SetDressRules(setRules.PendingStudents, setRules.PendingStaff, setRules.Result),
setRules.Result);
break;
case GameCommand.GetSpeechRules getSpeech:
HandleSpeechRules(getSpeech.SchoolId, new WorkerCommand.GetSpeechRules(getSpeech.Result), getSpeech.Result);
break;
case GameCommand.SetSpeechRules setSpeech:
HandleSpeechRules(
setSpeech.SchoolId,
new WorkerCommand.SetSpeechRules(setSpeech.PendingStudents, setSpeech.PendingStaff, setSpeech.Result),
setSpeech.Result);
break;
}
}
@@ -328,6 +339,14 @@ internal sealed class GameLoopService(
}
}
private void HandleSpeechRules(int schoolId, WorkerCommand command, TaskCompletionSource<SpeechRulesOutcome> result)
{
if (!_workers.TryGetValue(schoolId, out var worker) || !worker.Post(command))
{
result.TrySetResult(SpeechRulesOutcome.Fail(SpeechRulesError.UnknownSchool));
}
}
private void HandleStaffing(int schoolId, WorkerCommand command, TaskCompletionSource<StaffingOutcome> result)
{
if (!_workers.TryGetValue(schoolId, out var worker) || !worker.Post(command))
@@ -700,6 +719,7 @@ internal sealed class GameLoopService(
createSeed: null,
save.Presence,
save.DressRules,
save.SpeechRules,
save.Owner,
save.PortraitSettings);
worker.Start();
@@ -758,6 +778,7 @@ internal sealed class GameLoopService(
int? createSeed = null,
IReadOnlyList<PresenceSnapshot>? presence = null,
SchoolDressRules? dressRules = null,
SchoolSpeechRules? speechRules = null,
string? owner = null,
SwarmUiConfigFile? portraitSettings = null) =>
new(
@@ -775,6 +796,7 @@ internal sealed class GameLoopService(
createSeed,
presence,
dressRules,
speechRules,
owner,
portraitSettings,
_options,
+3
View File
@@ -36,6 +36,8 @@ internal sealed class SchoolSave
public SchoolDressRules? DressRules { get; init; }
public SchoolSpeechRules? SpeechRules { get; init; }
/// <summary>Normalized player name. Missing or blank means ownerless.</summary>
public string? Owner { get; init; }
@@ -179,6 +181,7 @@ internal sealed class SchoolStore
NativeLanguage = save.NativeLanguage,
Presence = save.Presence,
DressRules = save.DressRules,
SpeechRules = save.SpeechRules,
Owner = save.Owner,
PortraitSettings = save.PortraitSettings,
});
@@ -125,6 +125,29 @@ internal sealed partial class SchoolWorker
dirty = true;
break;
}
case WorkerCommand.GetSpeechRules getSpeech:
getSpeech.Result.TrySetResult(SpeechRulesOutcome.Ok(school.SpeechRules));
break;
case WorkerCommand.SetSpeechRules setSpeech:
{
var next = school.SpeechRules;
if (setSpeech.PendingStudents is { } students)
{
next = next with { PendingStudents = students };
}
if (setSpeech.PendingStaff is { } staff)
{
next = next with { PendingStaff = staff };
}
school.SpeechRules = next;
setSpeech.Result.TrySetResult(SpeechRulesOutcome.Ok(school.SpeechRules));
dirty = true;
break;
}
}
}
catch (Exception ex)
@@ -184,6 +207,12 @@ internal sealed partial class SchoolWorker
case WorkerCommand.SetDressRules setRules:
setRules.Result.TrySetResult(DressRulesOutcome.Fail(DressRulesError.UnknownSchool));
break;
case WorkerCommand.GetSpeechRules getSpeech:
getSpeech.Result.TrySetResult(SpeechRulesOutcome.Fail(SpeechRulesError.UnknownSchool));
break;
case WorkerCommand.SetSpeechRules setSpeech:
setSpeech.Result.TrySetResult(SpeechRulesOutcome.Fail(SpeechRulesError.UnknownSchool));
break;
}
}
@@ -221,6 +250,12 @@ internal sealed partial class SchoolWorker
case WorkerCommand.SetDressRules setRules:
setRules.Result.TrySetException(exception);
break;
case WorkerCommand.GetSpeechRules getSpeech:
getSpeech.Result.TrySetException(exception);
break;
case WorkerCommand.SetSpeechRules setSpeech:
setSpeech.Result.TrySetException(exception);
break;
}
}
@@ -207,6 +207,7 @@ internal sealed partial class SchoolWorker
NativeLanguage = _nativeLanguage,
Presence = school.CapturePresence(),
DressRules = school.DressRules,
SpeechRules = school.SpeechRules,
Owner = _owner,
PortraitSettings = _portraitSettings,
});
@@ -94,6 +94,7 @@ internal sealed partial class SchoolWorker
_school = school;
school.DressRules = _savedDressRules ?? new SchoolDressRules();
school.SpeechRules = _savedSpeechRules ?? new SchoolSpeechRules();
PublishSnapshot();
if (_isNew)
+3
View File
@@ -35,6 +35,7 @@ internal sealed partial class SchoolWorker
private readonly int? _createSeed;
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
private readonly SchoolDressRules? _savedDressRules;
private readonly SchoolSpeechRules? _savedSpeechRules;
private readonly string? _owner;
private readonly SwarmUiConfigFile? _portraitSettings;
private readonly Action<int> _onFailed;
@@ -73,6 +74,7 @@ internal sealed partial class SchoolWorker
int? createSeed,
IReadOnlyList<PresenceSnapshot>? savedPresence,
SchoolDressRules? savedDressRules,
SchoolSpeechRules? savedSpeechRules,
string? owner,
SwarmUiConfigFile? portraitSettings,
SimulationOptions options,
@@ -97,6 +99,7 @@ internal sealed partial class SchoolWorker
_createSeed = createSeed;
_savedPresence = savedPresence;
_savedDressRules = savedDressRules;
_savedSpeechRules = savedSpeechRules;
_owner = string.IsNullOrWhiteSpace(owner) ? null : owner;
_portraitSettings = portraitSettings;
_options = options;
+7
View File
@@ -71,4 +71,11 @@ internal abstract record WorkerCommand
DressRulePair? PendingStudents,
DressRulePair? PendingStaff,
TaskCompletionSource<DressRulesOutcome> Result) : WorkerCommand;
internal sealed record GetSpeechRules(TaskCompletionSource<SpeechRulesOutcome> Result) : WorkerCommand;
internal sealed record SetSpeechRules(
string? PendingStudents,
string? PendingStaff,
TaskCompletionSource<SpeechRulesOutcome> Result) : WorkerCommand;
}