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

Update API for session rework #11

Closed
Closed
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
15 changes: 15 additions & 0 deletions Source/API/Dummy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,20 @@ public IPlayerInfo GetPlayerById(int id)
{
throw new UninitializedAPI();
}

public ISessionManager GetGlobalSessionManager()
{
throw new UninitializedAPI();
}

public ISessionManager GetLocalSessionManager(Map map)
{
throw new UninitializedAPI();
}

public void SetCurrentSessionWithTransferables(ISessionWithTransferables session)
{
throw new UninitializedAPI();
}
}
}
136 changes: 136 additions & 0 deletions Source/API/Interfaces.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using RimWorld;
using Verse;

namespace Multiplayer.API
Expand Down Expand Up @@ -542,13 +543,18 @@ public interface IAPI

void RegisterDialogNodeTree(MethodInfo method);

[Obsolete($"Use {nameof(Session)} instead.")]
void RegisterPauseLock(PauseLockDelegate pauseLock);

Thing GetThingById(int id);
bool TryGetThingById(int id, out Thing value);

IReadOnlyList<IPlayerInfo> GetPlayers();
IPlayerInfo GetPlayerById(int id);

ISessionManager GetGlobalSessionManager();
ISessionManager GetLocalSessionManager(Map map);
void SetCurrentSessionWithTransferables(ISessionWithTransferables session);
}

/// <summary>
Expand Down Expand Up @@ -588,4 +594,134 @@ public interface IPlayerInfo
/// </summary>
IReadOnlyList<Thing> SelectedThings { get; }
}

public interface ISessionManager
{
/// <summary>
/// Returns the list of all currently active sessions for this specific <see cref="ISessionManager"/>.
/// </summary>
IReadOnlyList<Session> AllSessions { get; }
/// <summary>
/// Returns the list of all currently active exposable sessions for this specific <see cref="ISessionManager"/>.
/// </summary>
IReadOnlyList<ExposableSession> ExposableSessions { get; }
/// <summary>
/// Returns the list of all currently active semi-persistent sessions for this specific <see cref="ISessionManager"/>.
/// </summary>
IReadOnlyList<SemiPersistentSession> SemiPersistentSessions { get; }
/// <summary>
/// Returns the list of all currently active ticking sessions for this specific <see cref="ISessionManager"/>.
/// </summary>
IReadOnlyList<ITickingSession> TickingSessions { get; }
/// <summary>
/// A convenience property for checking if any of the sessions is active.
/// </summary>
bool AnySessionActive { get; }

/// <summary>
/// Adds a new session to the list of active sessions.
/// </summary>
/// <param name="session">The session to try to add to active sessions.</param>
/// <returns><see langword="true"/> if the session was added to active ones, <see langword="false"/> if there was a conflict between sessions.</returns>
bool AddSession(Session session);

/// <summary>
/// Tries to get a conflicting session (through the use of <see cref="ISessionWithCreationRestrictions"/>) or, if there was none, returns the input <paramref name="session"/>.
/// </summary>
/// <param name="session">The session to try to add to active sessions.</param>
/// <returns>A session that was conflicting with the input one, or the input itself if there were no conflicts. It may be of a different type than the input.</returns>
Session GetOrAddSessionAnyConflict(Session session);

/// <summary>
/// Tries to get a conflicting session (through the use of <see cref="ISessionWithCreationRestrictions"/>) or, if there was none, returns the input <paramref name="session"/>.
/// </summary>
/// <param name="session">The session to try to add to active sessions.</param>
/// <returns>A session that was conflicting with the input one if it's the same type (<c>other is T</c>), null if it's a different type, or the input itself if there were no conflicts.</returns>
T GetOrAddSession<T>(T session) where T : Session;

/// <summary>
/// Tries to remove a session from active ones.
/// </summary>
/// <param name="session">The session to try to remove from the active sessions.</param>
/// <returns><see langword="true"/> if successfully removed from <see cref="AllSessions"/>. Doesn't correspond to if it was successfully removed from other lists of sessions.</returns>
bool RemoveSession(Session session);

/// <summary>
/// Returns the first active session of specific type.
/// </summary>
/// <typeparam name="T">Type of the session to retrieve.</typeparam>
/// <returns>The first session of specified type, or <see langword="null"/> if there are none.</returns>
T GetFirstOfType<T>() where T : Session;

/// <summary>
/// Returns the session with specific ID of specific type.
/// </summary>
/// <param name="id">The ID of the session to search for.</param>
/// <typeparam name="T">Type of the session to retrieve.</typeparam>
/// <returns>The session with provided ID and of specified type, or <see langword="null"/> if there are none.</returns>
T GetFirstWithId<T>(int id) where T : Session;

/// <summary>
/// Returns the session with specific ID.
/// </summary>
/// <param name="id">The ID of the session to search for.</param>
/// <returns>The session with provided ID, or <see langword="null"/> if there are none.</returns>
Session GetFirstWithId(int id);

/// <summary>
/// Checks if any of active sessions is currently pausing the game.
/// </summary>
/// <param name="map">The map at which the sessions would check if the game is paused. Global session manager accepts <see langword="null"/> for global pausing.</param>
/// <returns><see langword="true"/> if any session is active, <see langword="false"/> otherwise.</returns>
/// <remarks>Local session managers expect the <paramref name="map"/> to be the same as the map it's attached to.</remarks>
bool IsAnySessionCurrentlyPausing(Map map); // Is it necessary for the API?
}

/// <summary>
/// <para>Required by sessions dealing with transferables, like trading or caravan forming. By implementing this interface, Multiplayer will handle majority of syncing of changes in transferables.</para>
/// <para>When drawing the dialog tied to this session, you'll have to set <see cref="MP.SetCurrentSessionWithTransferables"/> to the proper session, and set it to null once done.</para>
/// </summary>
/// <remarks>For safety, make sure to set <see cref="MP.SetCurrentSessionWithTransferables"/> in <see langword="try"/> and unset in <see langword="finally"/>.</remarks>
public interface ISessionWithTransferables
{
/// <summary>
/// Used when syncing data across players, specifically to retrieve <see cref="Transferable"/> based on the <see cref="Thing"/> it has.
/// </summary>
/// <param name="thingId"><see cref="Thing.thingIDNumber"/> of the <see cref="Thing"/>.</param>
/// <returns><see cref="Transferable"/> which corresponds to a <see cref="Thing"/> with specific <see cref="Thing.thingIDNumber"/>.</returns>
Transferable GetTransferableByThingId(int thingId);

/// <summary>
/// Called when the count in a specific <see cref="Transferable"/> was changed.
/// </summary>
/// <param name="tr">Transferable whose count was changed.</param>
void Notify_CountChanged(Transferable tr);
}

/// <summary>
/// Interface used by sessions that have restrictions based on other existing sessions, for example limiting them to only 1 session of specific type.
/// </summary>
public interface ISessionWithCreationRestrictions
{
/// <summary>
/// <para>Method used to check if the current session can be created by checking other <see cref="ISession"/>.</para>
/// <para>Only sessions in the current context are checked (local map sessions or global sessions).</para>
/// </summary>
/// <param name="other">The other session the current one is checked against. Can be of different type.</param>
/// <remarks>Currently only the current class checks against the existing ones - the existing classed don't check against this one.</remarks>
/// <returns><see langword="true"/> if the current session should be created, <see langword="false"/> otherwise</returns>
bool CanExistWith(Session other);
}

/// <summary>
/// Used by sessions that are are required to tick together with the map/world.
/// </summary>
public interface ITickingSession
{
/// <summary>
/// Called once per session when the map (for local sessions) or the world (for global sessions) is ticking.
/// </summary>
/// <remarks>The sessions are iterated over backwards using a for loop, so it's safe for them to remove themselves from the session manager.</remarks>
void Tick();
}
}
37 changes: 37 additions & 0 deletions Source/API/MP.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ static MP()
/// RegisterPauseLock(map => MyOtherClass.shouldPause);
/// </code>
/// </example>
[Obsolete($"Use {nameof(Session)} instead.")]
public static void RegisterPauseLock(PauseLockDelegate pauseLock) => Sync.RegisterPauseLock(pauseLock);

/// <summary>
Expand Down Expand Up @@ -303,5 +304,41 @@ static MP()
/// <param name="id"><see cref="IPlayerInfo.Id"/> of the player to retrieve</param>
/// <returns>Player with specified ID number</returns>
public static IPlayerInfo GetPlayerById(int id) => Sync.GetPlayerById(id);

/// <summary>
/// Retrieves the global (world) session manager.
/// </summary>
/// <returns>The global (world) session manager.</returns>
/// <remarks>As long as a multiplayer session is active, there should always be a global session manager. This method should never return <see langword="null"/> in such cases, unless something is very broken.</remarks>
public static ISessionManager GetGlobalSessionManager() => Sync.GetGlobalSessionManager();
/// <summary>
/// Retrieves the local (map) session manager.
/// </summary>
/// <param name="map">The map whose session manager will be retrieved.</param>
/// <returns>The local (map) session manager.</returns>
/// <remarks>As long as a multiplayer session is active, all maps should contain a session manager. This method should never return <see langword="null"/> in such cases, unless something is very broken.</remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="map"/> is null.</exception>
public static ISessionManager GetLocalSessionManager(Map map) => Sync.GetLocalSessionManager(map);
/// <summary>
/// <para>Sets the currently active session with transferables. Used for syncing changes in transferables by letting Multiplayer know which session should be synced.</para>
/// <para>It cannot be set to anything but <see langword="null"/> while a session is currently set as active.</para>
/// <para>The session needs to be set before (potentially) operating on the trasnferables, and unset afterwards.</para>
/// <para>The session should be set/unset in a <see langword="try"/><see langword="finally"/> block.</para>
/// </summary>
/// <param name="session">The session to set as the active one, or <see langword="null"/> to unset.</param>
/// <example>
/// <code>
/// try
/// {
/// MP.SetCurrentSessionWithTransferables(session);
/// OperateOnTransferables();
/// }
/// finally
/// {
/// MP.SetCurrentSessionWithTransferables(null);
/// }
/// </code>
/// </example>
public static void SetCurrentSessionWithTransferables(ISessionWithTransferables session) => Sync.SetCurrentSessionWithTransferables(session);
}
}
129 changes: 129 additions & 0 deletions Source/API/MPTypes.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Reflection;
using RimWorld;
using RimWorld.Planet;
using Verse;

namespace Multiplayer.API
{
Expand Down Expand Up @@ -153,4 +156,130 @@ public static implicit operator SyncType(Type type)
return new SyncType(type);
}
}

/// <summary>
/// <para>Used by Multiplayer's session manager to allow for creation of blocking dialogs, while (in case of async time) only pausing specific maps.</para>
/// <para>Sessions will be reset/reloaded during reloading - to prevent it, implement <see cref="IExposableSession"/> or <see cref="ISemiPersistentSession"/>.</para>
/// <para>You should avoid implementing this interface directly, instead opting into inheriting <see cref="Session"/> for greater compatibility.</para>
/// </summary>
public abstract class Session
{
// Use internal to prevent mods from easily modifying it?
protected int sessionId;
// Should it be virtual?
/// <summary>
/// <para>Used for syncing session across players by assigning them IDs, similarly to how every <see cref="Thing"/> receives an ID.</para>
/// <para>Automatically applied by the session manager</para>
/// <para>If inheriting <see cref="Session"/> you don't have to worry about this property.</para>
/// </summary>
public int SessionId
{
get => sessionId;
set => sessionId = value;
}

/// <summary>
/// Used by the session manager while joining the game - if it returns <see langword="false"/> it'll get removed.
/// </summary>
public virtual bool IsSessionValid => true;

/// <summary>
/// Mandatory constructor for any subclass of <see cref="Session"/>.
/// </summary>
/// <param name="map">The map this session belongs to. It will be provided by session manager when syncing.</param>
protected Session(Map map) { }

/// <summary>
/// Called once the sessions has been added to the list of active sessions. Can be used for initialization.
/// </summary>
/// <remarks>In case of <see cref="ISessionWithCreationRestrictions"/>, this will only be called if successfully added.</remarks>
public virtual void PostAddSession()
{
}

/// <summary>
/// Called once the sessions has been removed to the list of active sessions. Can be used for cleanup.
/// </summary>
public virtual void PostRemoveSession()
{
}

/// <summary>
/// A convenience method to switch to a specific map or world. Intended to be used from <see cref="GetBlockingWindowOptions"/> when opening menu.
/// </summary>
/// <param name="map">Map to switch to or <see langword="null"/> to switch to world view.</param>
protected static void SwitchToMapOrWorld(Map map)
{
if (map == null)
{
Find.World.renderer.wantedMode = WorldRenderMode.Planet;
}
else
{
if (WorldRendererUtility.WorldRenderedNow) CameraJumper.TryHideWorld();
Current.Game.CurrentMap = map;
}
}

/// <summary>
/// The map this session is used by or <see langword="null"/> in case of global sessions.
/// </summary>
public abstract Map Map { get; }

/// <summary>
/// <para>Called when checking ticking and if any session returns <see langword="true"/> - it'll force pause the map/game.</para>
/// <para>In case of local (map) sessions, it'll only be called by the current map. In case of global (world) sessions, it'll be called by the world and each map.</para>
/// </summary>
/// <param name="map">Current map (when checked from local session manager) or <see langword="null"/> (when checked from local session manager).</param>
/// <remarks>If there are multiple sessions active, this method is not guaranteed to run if a session before this one returned <see langword="true"/>.</remarks>
/// <returns><see langword="true"/> if the session should pause the map/game, <see langword="false"/> otherwise.</returns>
public abstract bool IsCurrentlyPausing(Map map);

/// <summary>
/// Called when a session is active, and if any session returns a non-null value, a button will be displayed which will display all options.
/// </summary>
/// <param name="entry">Currently processed colonist bar entry. Will be called once per <see cref="ColonistBar.Entry.group"/>.</param>
/// <returns>Menu option that will be displayed when the session is active. Can be <see langword="null"/>.</returns>
public abstract FloatMenuOption GetBlockingWindowOptions(ColonistBar.Entry entry);
}

/// <summary>
/// <para>Sessions inheriting from this class contain persistent data.</para>
/// <para>When inheriting from this class, remember to call <c>base.ExposeData()</c> to let it handle <see cref="Session.SessionId"/></para>
/// <para>Persistent data:</para>
/// <list type="bullet">
/// <item>Serialized into XML using RimWorld's Scribe system</item>
/// <item>Save-bound: survives a server restart</item>
/// </list>
/// </summary>
public abstract class ExposableSession : Session, IExposable
{
/// <inheritdoc cref="Session(Map)"/>
protected ExposableSession(Map map) : base(map) { }

public virtual void ExposeData()
{
Scribe_Values.Look(ref sessionId, "sessionId");
}
}

/// <summary>
/// <para>Sessions implementing this interface consist of semi-persistent data.</para>
/// <para>Semi-persistent data:</para>
/// <list type="bullet">
/// <item>Serialized into binary using the Sync system</item>
/// <item>Session-bound: survives a reload, lost when the server is closed</item>
/// </list>
/// </summary>
public abstract class SemiPersistentSession : Session
{
/// <inheritdoc cref="Session(Map)"/>
protected SemiPersistentSession(Map map) : base(map) { }

/// <summary>
/// Writes/reads the data used by this session.
/// </summary>
/// <param name="sync">Sync worker used for writing/reading the data.</param>
public abstract void Sync(SyncWorker sync);
}
}