Your cool new gamemode must be added to the arena.registeredGameModes. You can do this using the method, arena.AddExternalGameModes(string, ExternalOnlineGameMode). You can hook it pretty much anywhere as long as it's coming before the ArenaOnlineLobbyMenu ctor. Preferably, hook Meadow's ArenaOnlineGameMode ctor and call the method at the end.
- Make a new file that includes your hooks and plugin information:
// MyCoolNewModPlugin.cs
using BepInEx;
using IL;
using RainMeadow;
using System;
using System.Security.Permissions;
using UnityEngine;
//#pragma warning disable CS0618
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
namespace MyNamespace
{
[BepInPlugin("YOUR_USERNAME.YOUR_PLUGIN_NAME", "FRIENDLY_PLUGIN_NAME", "0.1.0")]
public partial class MyMod : BaseUnityPlugin
{
public static MyMod instance;
private bool init;
private bool fullyInit;
private bool addedMod = false;
public void OnEnable()
{
instance = this;
On.RainWorld.OnModsInit += RainWorld_OnModsInit;
}
private void RainWorld_OnModsInit(On.RainWorld.orig_OnModsInit orig, RainWorld self)
{
orig(self);
if (init) return;
init = true;
try
{
// Option 1: Hook the Lobby ctor
new Hook(typeof(Lobby).GetMethod("ActivateImpl", BindingFlags.NonPublic | BindingFlags.Instance), (Action<Lobby> orig, Lobby self) =>
{
orig(self);
OnlineManager.lobby.AddData(new myNewGamemode());
});
new Hook(typeof(ArenaOnlineGameMode).GetMethod("AddClientData", BindingFlags.Public | BindingFlags.Instance), (Action<ArenaOnlineGameMode> orig, ArenaOnlineGameMode self) =>
{
orig(self);
self.clientSettings.AddData(new myNewGamemodeClientSettings()); // optional if you have client settings
});
// On.Menu.ctor += Menu_ctor;
fullyInit = true;
}
catch (Exception e)
{
Logger.LogError(e);
fullyInit = false;
}
}
// Option 2: Hook after Menu_ctor
private void Menu_ctor(On.Menu.Menu.orig_ctor orig, Menu.Menu self, ProcessManager manager, ProcessManager.ProcessID ID)
{
orig(self, manager, ID);
if (self is ArenaOnlineLobbyMenu)
{
AddNewMode();
}
}
private void AddNewMode()
{
if (RainMeadow.RainMeadow.isArenaMode(out var arena))
{
arena.AddExternalGameModes(MyNewExternalArenaGameMode.MyGameModeName, new myNewGamemode());
}
}
}
}- Make a file that includes your mod's inheritance from Arena's ExternalGameMode:
// ExternalCoolGame.cs
using RainMeadow;
using System.Text.RegularExpressions;
using Menu;
namespace MyNamespace
{
public class MyCoolNewGameMode : ExternalArenaGameMode
{
public static ArenaSetup.GameTypeID MyGameModeName = new ArenaSetup.GameTypeID("MyGameModeName", register: false);
}
}- Override the arena.externalGameMode's
GetGameModeId(NOTE: Must match enum's value you set inarena.registeredGameModes)
// in the class MyCoolNewGameMode
public override ArenaSetup.GameTypeID GetGameModeId
{
get
{
return MyGameModeName; // Set to YOUR cool game mode
}
set { GetGameModeId = value; }
}- Your new game mode will now be accessible in the online Arena menu!
public static bool isMyCoolGameMode(ArenaOnlineGameMode arena, out MyCoolNewGameMode tb)
{
tb = null;
if (arena.currentGameMode == MyGameModeName.value)
{
tb = (arena.registeredGameModes.FirstOrDefault(x => x.Key == MyGameModeName.value).Value as MyCoolNewGameMode);
return true;
}
return false;
}For when you want to leverage the state-system for syncing variables. Consider adding "group=" to your state variable.
internal class MyCoolLobbyData : OnlineResource.ResourceData
{
public MyCoolLobbyData() { }
public override ResourceDataState MakeState(OnlineResource resource)
{
return new State(this, resource);
}
internal class State : ResourceDataState
{
[OnlineField(group = "myGroup")]
public bool isInGame;
public State() { }
// takes the current value in the State
public State(MyCoolLobbyData data, OnlineResource onlineResource)
{
ArenaOnlineGameMode arena = (onlineResource as Lobby).gameMode as ArenaOnlineGameMode;
bool myMode = MyCoolNewGameMode.isMyCoolGameMode(arena, out var coolMode);
if (myMode)
{
this.isInGame = coolMode.isInGame;
}
}
// read the value in the State and applies it
public override void ReadTo(OnlineResource.ResourceData data, OnlineResource resource)
{
var lobby = (resource as Lobby);
bool myMode = MyCoolNewGameMode.isMyCoolGameMode(arena, out var coolMode);
if (myMode)
{
coolMode.isInGame = this.isInGame;
}
}
public override Type GetDataType() => typeof(MyCoolLobbyData);
}
}Field-groups are a way to organize fields in groups that each have that boolean flag for sent-or-skipped.
Each group means an extra bool that is continuously sent on every message about that resource/entity.
If any field in the field-group has changed, that entire field-group will be re-sent (because that's more efficient that just supporting every single field be optional).
// in the class MyCoolNewGameMode
public override void ResourceAvailable(OnlineResource onlineResource)
{
base.ResourceAvailable(onlineResource);
if (onlineResource is Lobby lobby)
{
lobby.AddData(new MyCoolLobbyData());
}
}Check ArenaLobbyData for example utilization.
public class MyClientSettings : OnlineEntity.EntityData
{
public int someonesNumber;
public MyClientSettings() { }
public override EntityDataState MakeState(OnlineEntity entity, OnlineResource inResource)
{
return new State(this);
}
public class State : EntityDataState
{
[OnlineField]
public int someonesNumber;
public State() { }
public State(MyClientSettings onlineEntity) : base()
{
this.someonesNumber = onlineEntity.someonesNumber;
}
public override void ReadTo(OnlineEntity.EntityData entityData, OnlineEntity onlineEntity)
{
var avatarSettings = (MyClientSettings)entityData;
avatarSettings.someonesNumber = this.someonesNumber;
}
public override Type GetDataType() => typeof(MyClientSettings);
}
}// in the class MyCoolNewGameMode
public override void AddClientData()
{
clientSettings.AddData( new MyClientSettings());
}
ExternalArenaGameMode provides access to the ArenaOnlineLobbyMenu with the following methods:
OnUIEnabled, OnUIDisabled, OnUIUpdate, OnUIShutdowdn
# NOTE: OnUIDisabled can be called from the menu's ctor, check for null references if used
base.OnUIEnabled(menu);
myTab = menu.arenaMainLobbyPage.tabContainer.AddTab("My Tab");
myTab.AddObjects(myInterface = new MyCoolNewInterface((ArenaMode)OnlineManager.lobby.gameMode, this, myTab.menu, myTab, new(0, 0), menu.arenaMainLobbyPage.tabContainer.size));base.OnUIDisabled(menu);
myCoolNewInterface?.OnShutdown();
if (myTab != null) menu.arenaMainLobbyPage.tabContainer.RemoveTab(myTab);
myTab = null;
foreach (ArenaPlayerBox playerBox in menu.arenaMainLobbyPage.playerDisplayer?.GetSpecificButtons<ArenaPlayerBox>() ?? [])
{
if (!playerBoxes.TryGetValue(playerBox, out IfIMadeCoolObjectsInPlayerBoxes customBoxStuff)) continue;
playerBox.ClearMenuObject(customBoxStuff);
playerBoxes.Remove(playerBox);
}Leverage ExternalArenaGameMode's virtual functions
public override string AddIcon(ArenaOnlineGameMode arena, PlayerSpecificOnlineHud owner, SlugcatCustomization customization, OnlinePlayer player)
{
if (owner.clientSettings.owner == OnlineManager.lobby.owner)
{
return "ChieftainA";
}
return base.AddIcon(arena, owner, customization, player);
}
public override Color IconColor(ArenaOnlineGameMode arena, PlayerSpecificOnlineHud owner, SlugcatCustomization customization, OnlinePlayer player)
{
if (owner.PlayerConsideredDead)
{
return Color.grey;
}
if (arena.reigningChamps != null && arena.reigningChamps.list != null && arena.reigningChamps.list.Contains(player.id))
{
return Color.yellow;
}
return base.IconColor(arena, owner, customization, player);
}Slugcat Abilities Tab has support to add your custom settings for slugcats. First make your class from SettingsPage
public class MyCustomSettingsPage : SettingsPage
{
public OpTextBox mySlugcatFlightDurationTextBox;
public SimpleButton backButton;
public override string Name => "My Custom Name"; //this will appear on Select Settings Page
public MyCustomSettingsPage(Menu.Menu menu, MenuObject owner) : base(menu, owner)
{
mySlugcatFlightDurationTextBox = new(new Configurable<int>(myOptionsSave.Value), new Vector2(0, 0), 40);
mySlugcatFlightDurationTextBox.OnValueUpdate += (UIconfig config, string lastValue, string newValue) =>
{
//Doesnt have to appear here, just make sure lobby data gets changed accordingly somewhere
MyCoolLobbyData data = GetMyLobbyData();
data.mySlugcatFlightDuration = mySlugcatFlightDurationTextBox.valueInt;
};
}
public override void SelectAndCreateBackButtons(SettingsPage? previousSettingPage, bool forceSelectedObject)
{
if (backButton == null)
{
//Signal Text HAS to be OnlineSlugcatAbilitiesInterface.BACKTOSELECT to return to Select Settings Page
backButton = new SimpleButton(menu, this, menu.Translate("BACK"), OnlineSlugcatAbilitiesInterface.BACKTOSELECT, new Vector2(30, 30), new Vector2(80, 30));
AddObjects(backButton);
}
if (forceSelectedObject)
menu.selectedObject = backButton;
}
public override void CallForSync()
{
MyCoolLobbyData data = GetMyLobbyData();
data.mySlugcatFlightDuration = mySlugcatFlightDurationTextBox.valueInt;
}
public override void SaveInterfaceOptions()
{
myOptionsSave.mySlugcatFlightDuration.Value = mySlugcatFlightDurationTextBox.valueInt;
}
public override void Update()
{
MyCoolLobbyData data = GetMyLobbyData();
mySlugcatFlightDurationTextBox.greyedOut = SettingsDisabled
if (!mySlugcatFlightDurationTextBox.held)
mySlugcatFlightDurationTextBox.valueInt = data.mySlugcatFlightDuration;
}
}You can check OnlineSlugcatAbilitiesInterface.MSCSettings, OnlineSlugcatAbilitiesInterface.WatcherSettings here as an example
Once you made the class, you can start hooking
public void ApplyHooks()
{
new Hook(typeof(OnlineSlugcatAbilitiesInterface).GetMethod("AddAllSettings"), OnlineSlugcatAbilitiesInterface_AddAllSettings);
new Hook(typeof(ArenaMainLobbyPage).GetMethod("ShouldOpenSlugcatAbilitiesTab"), ArenaMainLobbyPage_ShouldOpenSlugcatAbilitiesTab);
}
public void OnlineSlugcatAbilitiesInterface_AddAllSettings(Action<OnlineSlugcatAbilitiesInterface, string> orig, OnlineSlugcatAbilitiesInterface self, string painCatName)
{
orig(self, painCatName);
MyCustomSettingsPage myCustomSettings = new(self.menu, self);
self.AddSettingsTab(myCustomSettings);
}
//This hook is optional. Slugcat Abilities Tab only appears when MSC or Watcher is on
//Add this if you want your settings to appear without MSC AND Watcher
public bool ArenaMainLobbyPage_ShouldOpenSlugcatAbilitiesTab(Func<ArenaMainLobbyPage, bool> orig, ArenaMainLobbyPage self)
{
return true;
}There are a number of virtual functions available for you in ExternalArenaGameMode to leverage for Arena gameplay. Check the BaseGameMode.cs for a full list. They are added as a convenience. If you don't want to use them, hook your own. Best of luck, and ping @UO when you've made a new game mode!