Some very useful tools for devs when creating mods for Content Warning
If you are using harmony to patch your methods, you may also utilize the CWAPI.HarmonyPatcher for easier patching of your methods. It removes all of the repeating and also has a way to automatically determine the type of the class you are trying to patch as well as the type of the patcher class
Usage example:
usingCWAPI;// for the HarmonyPatchernamespacePluginNamespace;// your plugin namespacepublicstaticclassYourGameClassPatch// name your pacther <ClassYouArePatching>Patch to utilize all of the features the patcher provides{// if you are also using the FeatureManager, I strongly advise you to call the Init method from YourFeatureNameFeature.Initialize methodinternalstaticvoidInit()// the init method you call to apply the patches{YourPlugin.Patcher.SaveInfo();// save the type of the current class and the type you are patching// this only works if you named your class with a suffix 'Patch'// if the method doesn't work on release build, try to use any of its overloads// if the class of the method you are patching is not inside the Assembly-CSharp.dll make sure you specify the correct assembly (without .dll extension)// if the class of the method you are patching is not inside the global namespace, make sure you specify the namespace of it (can be with or without the dot at the end)// if something still doesnt work and you cant figure out why, you can also use any of the overloadsYourPlugin.Patcher.Patch(nameof(MethodYouAreTryingToPatch),// self explanatoryprefix:nameof(Prefix));// there are plenty of other ways to use this patcher. I just showed the simplest one. You can check the source code and figure out which overload is the best for you}publicstaticboolPrefix()// this is just a normal Harmony prefix patch{returntrue;}}publicclassYourPlugin:BepInPlugin// this is only an example, not a full class{publicstaticHarmonyPatcherPatcher{get;privateset;}=default!;// create the public variable and set it to default!privatevoidAwake(){Patcher=new(PLUGIN_GUID,YourPlugin.Logger);// create the harmony patcher instance}}If you have a lot individual features in your mod, you can also try the CWAPI.FeatureManager! It has a method InitializeFeatures() which must be called when the mod is loading.
Features must inherit the CWAPI.Feature class and be marked with CWAPI.FeatureAttribute for the FeatureManager to find them and register.
Feature manager automatically handles:
- Enabling and disabling individual features
- Has the option to mark a feature as required
Usage exaple:
usingBepInEx;usingBepInEx.Configuration;usingCWAPI;// for all of the necessary classesnamespacePluginNamespace;// your plugin namespace[Feature]// add the FeatureAttributeinternalclassYourFeatureNameFeature:Feature<YourFeatureNameFeature>// inherit the feature class{publicoverrideBepInEx.Logging.ManualLogSourceLogSource=>YourPlugin.Logger;// use your loggerpublicoverrideboolRequired=>false;// (optional) if set to true, disallows the user to turn off the feature. The initialize method for it is always called. Default: false (user can disable your feature)publicoverridestringFeatureName=>"YourFeatureName";// name your feature. It can be with spaces or withoutpublicoverridestringFeatureDescription=>"Explain what your feature does";publicConfigEntry<float>SomeSetting{get;privateset;}=null!;// example settingpublicoverridevoidCreateConfig(ConfigSectionsection){// this function is always called. No matter if this feature is enabled or not// you can register your feature config hereSomeSetting=section.Bind(nameof(SomeSetting),// the name of the setting20f,// the default value of the setting""" The description of this setting """);// description}publicoverridevoidInitialize(){// this function is only called if the feature is enabled// some code that initializes your feature. Put the SingletonNetworkComponent GameObject creating here}}publicclassYourPlugin:BepInPlugin// this is only an example, not a full class{privatevoidAwake(){FeatureManagerManager=new(YourPlugin.Logger,Config);// initialize the feature managerManager.RegisterFeaturesFromAssembly();// find all of the featuresManager.InitializeFeatures();// initialize all of the features// you can also pass in a boolean to set if you'd like to automatically handle exceptions if any occour in the initializing process}}NetworkComponent<THandler, TParent> transient abstract class for simpler networking
Usage example:
usingMyceliumNetworking;// mycelium networking for CustomRPCAttributeusingCWAPI;// for NetworkComponent classusingUnityEngine;// for this example to compilenamespacePluginNamespace;// your plugin namespace// your class name inherit the NetworkComponent the GameObject you are networking oninternalclassYourNetworkHandler:NetworkComponent<YourNetworkHandler,Player>{protectedoverrideuintMOD_ID=>YourPlugin.MOD_ID;// your plugin MOD_ID. You can set it to any random number or you can use the provided hashing function with your PLUGIN_GUID// in your mod you might not need to write the namespace of the ManualLogSource, but if you get an Ambiguous reference error, add it.protectedoverrideBepInEx.Logging.ManualLogSourceLogSource=>YourPlugin.Logger;// change this to your real logger// EXAMPLE 1: a function that sets the oxygen of the specified player[CustomRPC]// the custom RPC you define which is fired for the specific Player (in this case, it depends from TParent) when the SendOxygen calls the Send function// this RPC is executed on all clients on one playervoidSetOxygen(floatoxygen){if(ParentComponent==null||ParentComponent.data==null)return;// ParentComponent is of type Player (the type which is passed when inheriting the NetworkComponent (TParent)ParentComponent.data.remainingOxygen=Mathf.Clamp(oxygen,0f,ParentComponent.data.maxOxygen);// for this example, we set the player oxygen to the given amount}publicstaticvoidSendOxygen(PlayertargetPlayer,floatoxygen){// use the Send function to send the custom RPCSend(targetPlayer,nameof(SetOxygen),ReliableType.Reliable,oxygen// after the first 3 arguments, you can pass your custom arguments that will be available to the SetOxygen function);}// EXAMPLE 2: a function that sets a players visor text to empty for only one client[CustomRPC]// this RPC is executed only on one client, on one playervoidClearVisor(){if(ParentComponent==null||ParentComponent.refs==null)return;// check for null to aviod NullReferenceExceptionParentComponent.refs.visor.visorFaceText.text="";// set the visor text}publicstaticvoidSendClearVisor(PlayertargetPlayer,Playerplayer){// we need to pass in the Player to clear the visor and a Player to select the client// 'player' is used to select the client, while the targetPlayer works like in Send() methodSendTarget(targetPlayer,nameof(ClearVisor),player,ReliableType.Reliable);}}publicstaticclassPlayerPatch// this is just an example class{publicstaticvoidStart_Prefix(Player__instance)// we also need to patch the players start method, to add our network handler to them{// do not forget !__instance.ai when dealing with the player// you might think that this class is used only for players but you'd be wrong// it is also used for monsters and you should check if the "player" is a monsterif(!__instance.ai&&__instance.gameObject.GetComponent<YourNetworkHandler>()==null)__instance.gameObject.AddComponent<YourNetworkHandler>();}}SingletonNetwokHandler<TParent> singleton abstract class for simpler networking
Usage example:
usingBepInEx;// for BepInPlugin classusingMyceliumNetworking;// mycelium networking for CustomRPCAttributeusingCWAPI;// for SingletonNetworkComponent classusingUnityEngine;// for this example to compilenamespacePluginNamespace;// your plugin namespace// your class name inherit the SingletonNetworkComponentinternalclassYourNetworkHandler:SingletonNetworkComponent<YourNetworkHandler>{protectedoverrideuintMOD_ID=>YourPlugin.MOD_ID;// your plugin MOD_ID. You can set it to any random number or you can use the provided hashing function with your PLUGIN_GUID// in your mod you might not need to write the namespace of the ManualLogSource, but if you get an Ambiguous reference error, add it.protectedoverrideBepInEx.Logging.ManualLogSourceLogSource=>YourPlugin.Logger;// change this to your real logger// EXAMPLE 1: setting all of the players health[CustomRPC]// the custom RPC you define which is fired once when the SendMaxHealth calls the Send function// this RPC is executed on every client (even the one that executed the SendMaxHealth methodvoidSetMaxHealth(floatmaxHealth){Player.PlayerData.maxHealth=maxHealth;// sets the static maxHealth value for this examplePlayerHandler.instance.playersAlive.ForEach(p =>p.data.health=Mathf.Clamp(p.data.health,0f,maxHealth));// removes health if the player has more than the maxHealth}publicstaticvoidSendMaxHealth(floatmaxHealth){// use the Send function to send the custom RPCSend(nameof(SetMaxHealth),ReliableType.Reliable,maxHealth// after the first 2 arguments, you can pass your custom arguments that will be available to the SetOxygen function);}// EXAMPLE 2: setting the gravity for a selected person[CustomRPC]// this RPC is only executed on one clientvoidSetGravity(floatgravity){// Player.localPlayer is the same as the player passed in to SendGravityPlayer.localPlayer.refs.controller.gravity=gravity;}publicstaticvoidSendGravity(Playerplayer,floatgravity){// a steamId is retrieved from the player object, then the RPC is executed on the specified clientSendTarget(nameof(SetGravity),player,ReliableType.Reliable,gravity);}}publicclassYourPlugin:BepInPlugin// this is only an example, not a full class{privatevoidAwake(){newGameObject("YourNetworkHandler",typeof(YourNetworkHandler));// you must register the game object for this network component to work}}usingSystem.Security.Cryptography;usingSystem.Text;usingSystem;publicclassProgram{publicstaticvoidMain(string[]args){byte[]bytes=Encoding.UTF8.GetBytes("YOUR.PLUGIN.GUID");// enter your mod plugin guid here and run this code. You can run this in a browseruinthash=0x811c9dc5;foreach(bytebinbytes){hash^=b;hash*=0x01000193;}Console.WriteLine(hash);}}