Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Psychic ritual syncing #482

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions Source/Client/Persistent/PsychicRitualBeginProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI.Group;

namespace Multiplayer.Client.Persistent;

public class PsychicRitualBeginProxy : Dialog_BeginPsychicRitual, ISwitchToMap
{
public static PsychicRitualBeginProxy drawing;

public PsychicRitualSession Session => map.MpComp().sessionManager.GetFirstOfType<PsychicRitualSession>();

public PsychicRitualBeginProxy(
PsychicRitualDef def,
PsychicRitualCandidatePool candidatePool,
PsychicRitualRoleAssignments assignments,
Map map) :
base(def, candidatePool, assignments, map)
{
var session = Session;

if (Session == null)
{
Log.Error("Trying to open a psychic ritual dialog proxy without session active");
return;
}

try
{
// Ensure that InitializeCast call is seeded, use session and map IDs to get a somewhat random value.
// We could also include current map tick as well, if needed.
Rand.PushState(Gen.HashCombineInt(session.SessionId, map.uniqueID));

// Recache the pending issues for the ritual.
// Each time the ritual is started, this method is called. However,
// in MP we can have multiple rituals active at a time, so ensure
// that we recache if the ritual is valid on a specific map.
// If there's ever issues with this, we may need to call this
// in DoWindowContents, however it shouldn't be needed.
def.InitializeCast(map);
}
finally
{
// Pop RNG state in finally to ensure no issues when an exception occurs.
Rand.PopState();
}
}

public override void DoWindowContents(Rect inRect)
{
drawing = this;

try
{
var session = Session;

if (session == null)
{
soundClose = SoundDefOf.Click;
Close();
}

base.DoWindowContents(inRect);
}
finally
{
drawing = null;
}
}

public override void Start()
{
if (CanBegin)
Session?.Start();
}
}
45 changes: 45 additions & 0 deletions Source/Client/Persistent/PsychicRitualPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using HarmonyLib;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI.Group;

namespace Multiplayer.Client.Persistent;

[HarmonyPatch(typeof(Widgets), nameof(Widgets.ButtonTextWorker))]
static class MakeCancelPsychicRitualButtonRed
{
static void Prefix(string label, ref bool __state)
{
if (PsychicRitualBeginProxy.drawing == null) return;
if (label != "CancelButton".Translate()) return;

GUI.color = new Color(1f, 0.3f, 0.35f);
__state = true;
}

static void Postfix(bool __state, ref Widgets.DraggableResult __result)
{
if (!__state) return;

GUI.color = Color.white;
if (__result.AnyPressed())
{
PsychicRitualBeginProxy.drawing.Session?.Remove();
__result = Widgets.DraggableResult.Idle;
}
}
}

[HarmonyPatch(typeof(PsychicRitualGizmo), nameof(PsychicRitualGizmo.InitializePsychicRitual))]
static class CancelDialogBeginPsychicRitual
{
static bool Prefix(PsychicRitualDef_InvocationCircle psychicRitualDef, Thing target)
{
if (Multiplayer.Client == null)
return true;

PsychicRitualSession.OpenOrCreateSession(psychicRitualDef, target);
return false;
}
}
120 changes: 120 additions & 0 deletions Source/Client/Persistent/PsychicRitualSession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Multiplayer.API;
using RimWorld;
using Verse;
using Verse.AI.Group;

namespace Multiplayer.Client.Persistent;

public class PsychicRitualSession : SemiPersistentSession, ISessionWithCreationRestrictions
{
public Map map;
public PsychicRitualDef ritual;
public PsychicRitualCandidatePool candidatePool;
public MpPsychicRitualAssignments assignments;

public override Map Map => map;

public PsychicRitualSession(Map map) : base(map)
{
this.map = map;
}

public PsychicRitualSession(Map map, PsychicRitualDef ritual, PsychicRitualCandidatePool candidatePool, MpPsychicRitualAssignments assignments) : this(map)
{
this.ritual = ritual;
this.assignments = assignments;
this.candidatePool = candidatePool;
this.assignments.session = this;
}

public static void OpenOrCreateSession(PsychicRitualDef_InvocationCircle ritual, Thing target)
{
// We need Find.CurrentMap to match the map we're creating the session in
var map = Find.CurrentMap;
if (map != target.Map)
{
Log.Error($"Error opening/creating {nameof(PsychicRitualSession)} - current map ({Find.CurrentMap}) does not match ritual spot map ({target.Map}).");
return;
}

var session = map.MpComp().sessionManager.GetFirstOfType<PsychicRitualSession>();
if (session == null)
CreateSession(ritual, target);
else
session.OpenWindow();
}

// Need CurrentMap for PsychicRitualDef.FindCandidatePool call
[SyncMethod(SyncContext.CurrentMap)]
public static void CreateSession(PsychicRitualDef_InvocationCircle ritual, Thing target)
{
var map = Find.CurrentMap;

// Get role assignments and candidate pool
var candidatePool = ritual.FindCandidatePool();
var assignments = MpUtil.ShallowCopy(ritual.BuildRoleAssignments(target), new MpPsychicRitualAssignments());

var manager = map.MpComp().sessionManager;
var session = manager.GetOrAddSession(new PsychicRitualSession(map, ritual, candidatePool, assignments));

if (TickPatch.currentExecutingCmdIssuedBySelf)
session.OpenWindow();
}

[SyncMethod]
public void Remove()
{
map.MpComp().sessionManager.RemoveSession(this);
}

[SyncMethod]
public void Start()
{
Remove();
ritual.MakeNewLord(assignments);
Find.PsychicRitualManager.RegisterCooldown(ritual);
}

public void OpenWindow(bool sound = true)
{
var dialog = new PsychicRitualBeginProxy(
ritual,
candidatePool,
assignments,
map);

if (!sound)
dialog.soundAppear = null;

Find.WindowStack.Add(dialog);
}

public override void Sync(SyncWorker sync)
{
sync.Bind(ref ritual);
sync.Bind(ref candidatePool);

SyncType assignmentsType = typeof(MpPsychicRitualAssignments);
assignmentsType.expose = true;
sync.Bind(ref assignments, assignmentsType);
assignments.session = this;
}

public override bool IsCurrentlyPausing(Map map) => map == this.map;

public override FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry)
{
return new FloatMenuOption("MpPsychicRitualSession".Translate(), () =>
{
SwitchToMapOrWorld(entry.map);
OpenWindow();
});
}

public bool CanExistWith(Session other) => other is not PsychicRitualSession;
}

public class MpPsychicRitualAssignments : PsychicRitualRoleAssignments
{
public PsychicRitualSession session;
}
83 changes: 82 additions & 1 deletion Source/Client/Syncing/Dict/SyncDictDlc.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Multiplayer.API;
using Multiplayer.Client.Persistent;
Expand Down Expand Up @@ -313,7 +314,87 @@ public static class SyncDictDlc

return (ActivityGizmo)comp.gizmo;
}
}
},
{
(ByteWriter data, PsychicRitualRoleAssignments assignments) =>
{
// In Multiplayer, PsychicRitualRoleAssignments should only be of the wrapper type MpRitualAssignments
var mpAssignments = (MpPsychicRitualAssignments)assignments;
data.MpContext().map = mpAssignments.session.map;
data.WriteInt32(mpAssignments.session.SessionId);
},
(ByteReader data) =>
{
var id = data.ReadInt32();
var ritual = data.MpContext().map.MpComp().sessionManager.GetFirstWithId<PsychicRitualSession>(id);
return ritual?.assignments;
}
},
{
(ByteWriter data, PsychicRitualCandidatePool candidatePool) =>
{
WriteSync(data, candidatePool.AllCandidatePawns);
WriteSync(data, candidatePool.NonAssignablePawns);
},
(ByteReader data) =>
{
var allCandidates = ReadSync<List<Pawn>>(data);
var nonAssignable = ReadSync<List<Pawn>>(data);

return new PsychicRitualCandidatePool(allCandidates, nonAssignable);
}
},
{
(ByteWriter data, PsychicRitual ritual) =>
{
var lordToil = ritual?.lord?.CurLordToil as LordToil_PsychicRitual;
WriteSync(data, lordToil);

// We could skip syncing the ID, I've decided to include it for additional error checking
if (lordToil != null)
data.WriteInt32(ritual.loadID);
},
(ByteReader data) =>
{
var lordToil = ReadSync<LordToil_PsychicRitual>(data);
if (lordToil == null)
return null;

var ritualId = data.ReadInt32();
var ritual = lordToil.RitualData.psychicRitual;

if (ritual == null)
{
Log.Error("Psychic ritual was null after syncing");
return null;
}
if (ritual.loadID != ritualId)
{
Log.Error($"Synced psychic ritual ID did not match after syncing, expected: {ritualId}, current: {ritual.loadID}");
return null;
}

return ritual;
}, true // implicit
},
{
// Currently only used for Dialog_BeginPsychicRitual delegate syncing
(ByteWriter data, PawnPsychicRitualRoleSelectionWidget dialog) =>
{
// psychicRitualAssignments and assignments fields store the same object.
// If we used assignments field we would have to cast it before syncing.
WriteSync(data, dialog.psychicRitualAssignments);
WriteSync(data, dialog.ritualDef);
},
(ByteReader data) =>
{
var assignments = (MpPsychicRitualAssignments)ReadSync<PsychicRitualRoleAssignments>(data);
if (assignments == null) return null;

var ritual = ReadSync<PsychicRitualDef>(data); // todo handle ritual becoming null?
return new PawnPsychicRitualRoleSelectionWidget(ritual, assignments.session.candidatePool, assignments);
}
},

#endregion
};
Expand Down
9 changes: 9 additions & 0 deletions Source/Client/Syncing/Game/SyncDelegates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Multiplayer.Client.Patches;
using MultiplayerLoader;
using Verse;
using Verse.AI.Group;

namespace Multiplayer.Client
{
Expand Down Expand Up @@ -271,6 +272,11 @@ private static void InitRituals()
SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 3); // Select role, set confirm text
SyncDelegate.Lambda(typeof(Dialog_BeginRitual), nameof(Dialog_BeginRitual.DrawRoleSelection), 4); // Select role, no confirm text

SyncMethod.Register(typeof(PsychicRitual), nameof(PsychicRitual.CancelPsychicRitual));
SyncMethod.Register(typeof(PsychicRitual), nameof(PsychicRitual.LeavePsychicRitual)); // Make pawn leave ritual

SyncMethod.Register(typeof(GameComponent_PsychicRitualManager), nameof(GameComponent_PsychicRitualManager.ClearAllCooldowns)).SetDebugOnly(); // Dev reset all psychic ritual cooldowns

/*
PawnRoleSelectionWidgetBase

Expand All @@ -287,6 +293,9 @@ The UI's main interaction area is split into three types of groups of pawns.
SyncMethod.Register(typeof(RitualRoleAssignments), nameof(RitualRoleAssignments.TryAssignSpectate));
SyncMethod.Register(typeof(RitualRoleAssignments), nameof(RitualRoleAssignments.RemoveParticipant));

SyncMethod.Register(typeof(PsychicRitualRoleAssignments), nameof(PsychicRitualRoleAssignments.TryAssignSpectate));
SyncMethod.Register(typeof(PsychicRitualRoleAssignments), nameof(PsychicRitualRoleAssignments.RemoveParticipant));

SyncRituals.ApplyPrepatches(null);
}

Expand Down
Loading
Loading