Skip to content

6fears7/Arena-Online

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 

Repository files navigation

Arena API

Background

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.

Registering a new game mode

  1. 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());
            }

        }
    }
}
  1. 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);
    }
}
  1. Override the arena.externalGameMode's GetGameModeId (NOTE: Must match enum's value you set in arena.registeredGameModes)
// in the class MyCoolNewGameMode
public override ArenaSetup.GameTypeID GetGameModeId
{
    get
    {
        return MyGameModeName; // Set to YOUR cool game mode
    }
    set { GetGameModeId = value; }
}
  1. Your new game mode will now be accessible in the online Arena menu!

GameMode Check

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;
}

State Data

For when you want to leverage the state-system for syncing variables. Consider adding "group=" to your state variable.

LobbyData

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);
        }
    }

What's with the "group='myGroup'"?

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.

ClientData

 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());
}

UI

Menu

Adding Menu Objects

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

Adding Tabs

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));

Remove Tabs

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);
}

UI - In-Game: Adding or Updating Custom Icons

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);
        }

Adding Slugcat Settings

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;
 }

Beyond

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!

About

Boilerplate and gamemodes for Rain World's Rain Meadow engine

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages