You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

550 lines
28 KiB

  1. <!DOCTYPE html>
  2. <!--[if IE]><![endif]-->
  3. <html>
  4. <head>
  5. <meta charset="utf-8">
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  7. <title>Making your own mod </title>
  8. <meta name="viewport" content="width=device-width">
  9. <meta name="title" content="Making your own mod ">
  10. <meta name="generator" content="docfx 2.48.0.0">
  11. <link rel="shortcut icon" href="../favicon.ico">
  12. <link rel="stylesheet" href="../styles/docfx.vendor.css">
  13. <link rel="stylesheet" href="../styles/docfx.css">
  14. <link rel="stylesheet" href="../styles/main.css">
  15. <link rel="stylesheet" href="../styles/fix.css">
  16. <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
  17. <meta property="docfx:navrel" content="../toc.html">
  18. <meta property="docfx:tocrel" content="toc.html">
  19. <meta property="docfx:rel" content="../">
  20. <meta property="docfx:newtab" content="true">
  21. </head> <body data-spy="scroll" data-target="#affix" data-offset="120">
  22. <div id="wrapper">
  23. <header>
  24. <nav id="autocollapse" class="navbar navbar-inverse ng-scope" role="navigation">
  25. <div class="container">
  26. <div class="navbar-header">
  27. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar">
  28. <span class="sr-only">Toggle navigation</span>
  29. <span class="icon-bar"></span>
  30. <span class="icon-bar"></span>
  31. <span class="icon-bar"></span>
  32. </button>
  33. <a class="navbar-brand" href="../index.html">
  34. <img id="logo" class="svg" src="../logo.svg" alt="">
  35. </a>
  36. </div>
  37. <div class="collapse navbar-collapse" id="navbar">
  38. <form class="navbar-form navbar-right" role="search" id="search">
  39. <div class="form-group">
  40. <input type="text" class="form-control" id="search-query" placeholder="Search" autocomplete="off">
  41. </div>
  42. </form>
  43. </div>
  44. </div>
  45. </nav>
  46. <div class="subnav navbar navbar-default">
  47. <div class="container hide-when-search" id="breadcrumb">
  48. <ul class="breadcrumb">
  49. <li></li>
  50. </ul>
  51. </div>
  52. </div>
  53. </header>
  54. <div class="container body-content">
  55. <div id="search-results">
  56. <div class="search-list"></div>
  57. <div class="sr-items">
  58. <p><i class="glyphicon glyphicon-refresh index-loading"></i></p>
  59. </div>
  60. <ul id="pagination"></ul>
  61. </div>
  62. </div>
  63. <div role="main" class="container body-content hide-when-search">
  64. <div class="sidenav hide-when-search">
  65. <a class="btn toc-toggle collapse" data-toggle="collapse" href="#sidetoggle" aria-expanded="false" aria-controls="sidetoggle">Show / Hide Table of Contents</a>
  66. <div class="sidetoggle collapse" id="sidetoggle">
  67. <div id="sidetoc"></div>
  68. </div>
  69. </div>
  70. <div class="article row grid-right">
  71. <div class="col-md-10">
  72. <article class="content wrap" id="_content" data-uid="articles.start.dev">
  73. <h1 id="making-a-mod">Making a mod</h1>
  74. <h2 id="overview">Overview</h2>
  75. <p>What follows is a <em>very</em> barebones, and frankly not very useful plugin class, even as a starting point,
  76. but it should be enough to give a decent idea of how to do quick upgrades of existing mods for those who want to.</p>
  77. <pre><code class="lang-cs" name="Plugin.cs">using System;
  78. using IPA;
  79. using IPA.Logging;
  80. namespace Demo
  81. {
  82. [Plugin(RuntimeOptions.SingleStartInit)]
  83. internal class Plugin
  84. {
  85. public static Logger log { get; private set; }
  86. [Init]
  87. public Plugin(Logger logger)
  88. {
  89. log = logger;
  90. log.Debug(&quot;Basic plugin running!&quot;);
  91. // setup that does not require game code
  92. // this is only called once ever, so do once-ever initialization
  93. }
  94. [OnStart]
  95. public void OnStart()
  96. {
  97. // setup that requires game code
  98. }
  99. [OnExit]
  100. public void OnExit()
  101. {
  102. // teardown
  103. }
  104. }
  105. }
  106. </code></pre><p>There are basically 4 major concepts here:</p>
  107. <ol>
  108. <li><a class="xref" href="../api/IPA.Logging.Logger.html">Logger</a>, the logging system.</li>
  109. <li><a class="xref" href="../api/IPA.PluginAttribute.html">PluginAttribute</a>, which declares that this class is a plugin and how it should behave.</li>
  110. <li><a class="xref" href="../api/IPA.InitAttribute.html">InitAttribute</a>, which declares the constructor (and optionally other methods) as being
  111. used for initialization.</li>
  112. <li>The lifecycle event attributes <a class="xref" href="../api/IPA.OnStartAttribute.html">OnStartAttribute</a> and <a class="xref" href="../api/IPA.OnExitAttribute.html">OnExitAttribute</a>.</li>
  113. </ol>
  114. <p>I reccommend you read the docs for each of those to get an idea for what they do.</p>
  115. <p>It is worth noting that this example is of a mod that <em>cannot</em> be enabled and disabled at runtime, as marked by
  116. <a class="xref" href="../api/IPA.RuntimeOptions.html#IPA_RuntimeOptions_SingleStartInit">RuntimeOptions.SingleStartInit</a>.</p>
  117. <h3 id="what-can-be-changed">What can be changed</h3>
  118. <p>Before we go adding more functionality, its worth mentioning that that is not the <em>only</em> way to have a plugin set up.</p>
  119. <p>For starters, we can add another <em>method</em> marked <code>[Init]</code>, and it will be called after the constructor, with the same
  120. injected parameters, if those are applicable.</p>
  121. <pre><code class="lang-cs" name="Plugin.cs#Init(Logger)">[Init]
  122. public void Init(Logger logger)
  123. {
  124. // logger will be the same instance as log currently is
  125. }
  126. </code></pre><p>If you only had a method marked <code>[Init]</code>, and no constructors marked <code>[Init]</code>, then the plugin type must expose a
  127. public default constructor. If multiple constructors are marked <code>[Init]</code>, only the one with the most parameters will
  128. be called.</p>
  129. <p>You may also mark as many methods as you wish with <code>[Init]</code> and all of them will be called, in no well-defined order on
  130. initialization. The same is true for <code>[OnStart]</code> and <code>[OnExit]</code>, respectively.</p>
  131. <h2 id="from-scratch">From Scratch</h2>
  132. <p>If you are starting from scratch, you will need one other thing to get your plugin up and running: a manifest.</p>
  133. <p>A basic manifest for that might look a little like this:</p>
  134. <pre><code class="lang-json" name="manifest.json">{
  135. &quot;author&quot;: &quot;ExampleMan&quot;,
  136. &quot;description&quot;: [
  137. &quot;A demo plugin written for the BSIPA basic tutorial.&quot;
  138. ],
  139. &quot;gameVersion&quot;: &quot;1.6.0&quot;,
  140. &quot;id&quot;: null,
  141. &quot;name&quot;: &quot;Demo Plugin&quot;,
  142. &quot;version&quot;: &quot;0.0.1&quot;,
  143. &quot;features&quot;: [
  144. ],
  145. &quot;links&quot;: {
  146. &quot;project-home&quot;: &quot;https://example.com/demo-plugin&quot;,
  147. &quot;project-source&quot;: &quot;https://github.com/exampleman/demo-plugin/&quot;,
  148. &quot;donate&quot;: &quot;https://ko-fi.com/exampleman&quot;
  149. },
  150. }
  151. </code></pre><p>There is a lot going on there, but most of it should be decently obvious. Among the things that <em>aren&#39;t</em> immediately obvious,
  152. are</p>
  153. <ul>
  154. <li><code>id</code>: This represents a unique identifier for the mod, for use by package managers such as BeatMods. It may be null if the
  155. mod chooses not to support those.</li>
  156. <li><code>features</code>: Don&#39;t worry about this for now, this is a not-very-simple thing that will be touched on later.</li>
  157. </ul>
  158. <p>In addition, there are a few gatchas with it:</p>
  159. <ul>
  160. <li><code>description</code>: This can be either a string or an array representing different lines. Markdown formatting is permitted.</li>
  161. <li><code>gameVersion</code>: This should match <em>exactly</em> with the application version of the game being targeted. While this is not enforced
  162. by BSIPA, mod repositories like BeatMods may require it match, and it is good practice regardless.</li>
  163. <li><code>version</code>: This must be a valid SemVer version number for your mod.</li>
  164. </ul>
  165. <p>In order for your plugin to load, the manifest must be embedded into the plugin DLL as an embedded resource. This can be set in
  166. the Visual Studio file properties panel under <code>Build Action</code>, or in the <code>.csproj</code> like so:</p>
  167. <pre><code class="lang-xml" name="Demo.csproj#manifest">&lt;ItemGroup&gt;
  168. &lt;EmbeddedResource Include=&quot;manifest.json&quot; /&gt;
  169. &lt;/ItemGroup&gt;
  170. </code></pre><p>At this point, if the main plugin source file and the manifest are in the same source location, and the plugin class is using the
  171. project&#39;s default namespace, the plugin will load just fine. However, this is somewhat difficult both to explain and verify, so I
  172. recommend you use the the <code>misc.plugin-hint</code> field in your manifest. It can be used like so:</p>
  173. <pre><code class="lang-json" name="manifest.json#misc.plugin-hint">&quot;misc&quot;: {
  174. &quot;plugin-hint&quot;: &quot;Demo.Plugin&quot;
  175. }
  176. </code></pre><p>With this, you can set <code>plugin-hint</code> to the full typename of your plugin type, and it will correctly load. This is a hint though,
  177. and will also try it as a namespace if it fails to find the plugin type. If that fails, it will then fall back to using the manifest&#39;s
  178. embedded namespace.</p>
  179. <h3 id="a-less-painful-description">A less painful description</h3>
  180. <p>If you want to have a relatively long or well-formatted description for your mod, it may start to become painful to embed it in a list
  181. of JSON strings in the manifest. Luckily, there is a way to handle this.</p>
  182. <p>The first step is to create another embedded file, but this time it should be a Markdown file, perhaps <code>description.md</code>. It may contain
  183. something like this:</p>
  184. <pre><code class="lang-markdown" name="description.md"># Demo Plugin
  185. A little demo for the BSIPA modding introduction.
  186. ---
  187. WE CAN USE MARKDOWN!!!
  188. </code></pre><p>Then, in your manifest description, have the first line be something look like this, but replacing <code>Demo.description.md</code> with the fully
  189. namespaced name of the resource:</p>
  190. <pre><code class="lang-json" name="manifest.json#description">&quot;#![Demo.description.md]&quot;,
  191. </code></pre><p>Now, when loaded into memory, if anything reads your description metadata, they get the content of that file instead of the content of the
  192. manifest key.</p>
  193. <h3 id="configuring-your-plugin">Configuring your plugin</h3>
  194. <p>Something that many plugins want and need is configuration. Fortunately, BSIPA provides a fairly powerful configuration system out of the
  195. box. To start using it, first create a config class of some kind. Lets take a look at a fairly simple example of this:</p>
  196. <pre><code class="lang-cs" name="PluginConfig.cs#basic">namespace Demo
  197. {
  198. public class PluginConfig
  199. {
  200. public static PluginConfig Instance { get; set; }
  201. public int IntValue { get; set; } = 42;
  202. public float FloatValue { get; set; } = 3.14159f;
  203. }
  204. }
  205. </code></pre><p>Notice how the class is both marked <code>public</code> <strong>and</strong> is not marked <code>sealed</code>. For the moment, both of these are necessary. Also notice that
  206. all of the members are properties. While this doesn&#39;t change much now, it will be significant in the near future.</p>
  207. <p>Now, how do we get this object off of disk? Simple. Back in your plugin class, change your <code>[Init]</code> constructor to look like this:</p>
  208. <pre><code class="lang-cs" name="Plugin.cs#config-init">[Init]
  209. public Plugin(Logger logger, Config conf)
  210. {
  211. log = logger;
  212. PluginConfig.Instance = conf.Generated&lt;PluginConfig&gt;();
  213. log.Debug(&quot;Config loaded&quot;);
  214. // setup that does not require game code
  215. }
  216. </code></pre><p>For this to compile, though, we will need to add a few <code>using</code>s:</p>
  217. <pre><code class="lang-cs" name="Plugin.cs#usings">using IPA.Config;
  218. using IPA.Config.Stores;
  219. </code></pre><p>With just this, you have your config automatically loading from disk! It&#39;s even reloaded when it gets changed mid-game! You can now access
  220. it from anywhere by simply accessing <code>PluginConfig.Instance</code>. Make sure you don&#39;t accidentally reassign this though, as then you will loose
  221. your only interaction with the user&#39;s preferences.</p>
  222. <p>By default, it will be named the same as is in your plugin&#39;s manifest&#39;s <code>name</code> field, and will use the built-in <code>json</code> provider. This means
  223. that the file that will be loaded from will be <code>UserData/Demo Plugin.json</code> for our demo plugin. You can, however, control both of those by
  224. applying attributes to the <a class="xref" href="../api/IPA.Config.Config.html">Config</a> parameter, namely <a class="xref" href="../api/IPA.Config.Config.NameAttribute.html">Config.NameAttribute</a> to control the name, and
  225. <a class="xref" href="../api/IPA.Config.Config.PreferAttribute.html">Config.PreferAttribute</a> to control the type. If the type preferences aren&#39;t registered though, it will just fall back to JSON.</p>
  226. <p>The config&#39;s behaviour can be found either later here, or in the remarks section of
  227. <a class="xref" href="../api/IPA.Config.Stores.GeneratedStore.html#IPA_Config_Stores_GeneratedStore_Generated__1_IPA_Config_Config_System_Boolean_">Generated&lt;T&gt;(Config, Boolean)</a>.</p>
  228. <p>At this point, your main plugin file should look something like this:</p>
  229. <pre><code class="lang-cs" name="Plugin.cs">using System;
  230. using IPA;
  231. using IPA.Logging;
  232. using IPA.Config;
  233. using IPA.Config.Stores;
  234. namespace Demo
  235. {
  236. [Plugin(RuntimeOptions.SingleStartInit)]
  237. internal class Plugin
  238. {
  239. public static Logger log { get; private set; }
  240. [Init]
  241. public Plugin(Logger logger, Config conf)
  242. {
  243. log = logger;
  244. PluginConfig.Instance = conf.Generated&lt;PluginConfig&gt;();
  245. log.Debug(&quot;Config loaded&quot;);
  246. // setup that does not require game code
  247. }
  248. [OnStart]
  249. public void OnStart()
  250. {
  251. // setup that requires game code
  252. }
  253. [OnExit]
  254. public void OnExit()
  255. {
  256. // teardown
  257. }
  258. }
  259. }
  260. </code></pre><hr>
  261. <p>But what about more complex types than just <code>int</code> and <code>float</code>? What if you want sub-objects?</p>
  262. <p>Those are supported natively, and so are very easy to set up. We just add this to the config class:</p>
  263. <pre><code class="lang-cs" name="PluginConfig.cs#sub-basic">public class SubThingsObject
  264. {
  265. public double DoubleValue { get; set; } = 2.718281828459045;
  266. }
  267. public SubThingsObject SubThings { get; set; } = new SubThingsObject();
  268. </code></pre><p>Now this object will be automatically read from disk too.</p>
  269. <p>But there is one caveat to this: because <code>SubThingsObject</code> is a reference type, <em><code>SubThings</code> can be null</em>.</p>
  270. <p>This is often undesireable. The obvious solution may be to simply change it to a <code>struct</code>, but that is both not supported <em>and</em> potentially
  271. undesirable for other reasons we&#39;ll get to later.</p>
  272. <p>Instead, you can use <a class="xref" href="../api/IPA.Config.Stores.Attributes.NonNullableAttribute.html">NonNullableAttribute</a>. Change the definition of <code>SubThings</code> to this:</p>
  273. <pre><code class="lang-cs" name="PluginConfig.cs#sub-basic-nonnull">[NonNullable]
  274. public SubThingsObject SubThings { get; set; } = new SubThingsObject();
  275. </code></pre><p>And add this to the <code>using</code>s:</p>
  276. <pre><code class="lang-cs" name="PluginConfig.cs#includes-attributes">using IPA.Config.Stores.Attributes;
  277. </code></pre><p>This attribute tells the serializer that <code>null</code> is an invalid value for the config object. This does, however, require that <em>you</em> take extra care
  278. ensure that it never becomes null in code, as that will break the serializer.</p>
  279. <hr>
  280. <p>What about collection types?</p>
  281. <p>Well, you can use those too, but you have to use something new: a converter.</p>
  282. <p>You may be familiar with them if you have used something like the popular Newtonsoft.Json library before. In BSIPA, they lie in the
  283. <a class="xref" href="../api/IPA.Config.Stores.Converters.html">IPA.Config.Stores.Converters</a> namespace. All converters either implement <a class="xref" href="../api/IPA.Config.Stores.IValueConverter.html">IValueConverter</a> or derive from
  284. <a class="xref" href="../api/IPA.Config.Stores.ValueConverter-1.html">ValueConverter&lt;T&gt;</a>. You will mostly use them with an <a class="xref" href="../api/IPA.Config.Stores.Attributes.UseConverterAttribute.html">UseConverterAttribute</a>.</p>
  285. <p>To use them, we&#39;ll want to import them:</p>
  286. <pre><code class="lang-cs" name="PluginConfig.cs#includes-attributes">using System.Collections.Generic;
  287. using IPA.Config.Stores;
  288. using IPA.Config.Stores.Converters;
  289. </code></pre><p>Then add a field, for example a list field:</p>
  290. <pre><code class="lang-cs" name="PluginConfig.cs#list-basic">[UseConverter(typeof(ListConverter&lt;string&gt;))]
  291. public List&lt;string&gt; ListValue { get; set; } = new List&lt;string&gt;();
  292. </code></pre><p>This uses a converter that is provided with BSIPA for <a class="xref" href="https://docs.microsoft.com/dotnet/api/system.collections.generic.list-1">List&lt;T&gt;</a>s specifically. It converts the list to
  293. an ordered array, which is then written to disk as a JSON array.</p>
  294. <p>We could also potentially want use something like a <a class="xref" href="https://docs.microsoft.com/dotnet/api/system.collections.generic.hashset-1">HashSet&lt;T&gt;</a>. Lets start by looking at the definition
  295. for such a member, then deciphering what exactly it means:</p>
  296. <pre><code class="lang-cs" name="PluginConfig.cs#set-basic">[UseConverter(typeof(CollectionConverter&lt;string, HashSet&lt;string&gt;&gt;))]
  297. public HashSet&lt;string&gt; SetValue { get; set; } = new HashSet&lt;string&gt;();
  298. </code></pre><p>The converter we&#39;re using here is <a class="xref" href="../api/IPA.Config.Stores.Converters.CollectionConverter-2.html">CollectionConverter&lt;T, TCollection&gt;</a>, a base type for converters of all kinds of
  299. collections. In fact, the <a class="xref" href="../api/IPA.Config.Stores.Converters.ListConverter-1.html">ListConverter&lt;T&gt;</a> is derived from this, and uses it for most of its implementation.
  300. If a type implements <a class="xref" href="https://docs.microsoft.com/dotnet/api/system.collections.generic.icollection-1">ICollection&lt;T&gt;</a>, <a class="xref" href="../api/IPA.Config.Stores.Converters.CollectionConverter-2.html">CollectionConverter&lt;T, TCollection&gt;</a> can convert it.</p>
  301. <p>It, like most other BSIPA provided aggregate converters, provides a type argument overload <a class="xref" href="../api/IPA.Config.Stores.Converters.CollectionConverter-3.html">CollectionConverter&lt;T, TCollection, TConverter&gt;</a>
  302. to compose other converters with it to handle unusual element types.</p>
  303. <p>Now after all that, your plugin class has not changed, and your config class should look something like this:</p>
  304. <pre><code class="lang-cs" name="PluginConfig.cs#basic-complete">using System.Collections.Generic;
  305. using IPA.Config.Stores;
  306. using IPA.Config.Stores.Attributes;
  307. using IPA.Config.Stores.Converters;
  308. namespace Demo
  309. {
  310. public class PluginConfig
  311. {
  312. public static PluginConfig Instance { get; set; }
  313. public class SubThingsObject
  314. {
  315. public double DoubleValue { get; set; } = 2.718281828459045;
  316. }
  317. public int IntValue { get; set; } = 42;
  318. public float FloatValue { get; set; } = 3.14159f;
  319. [NonNullable]
  320. public SubThingsObject SubThings { get; set; } = new SubThingsObject();
  321. [UseConverter(typeof(ListConverter&lt;string&gt;))]
  322. public List&lt;string&gt; ListValue { get; set; } = new List&lt;string&gt;();
  323. [UseConverter(typeof(CollectionConverter&lt;string, HashSet&lt;string&gt;&gt;))]
  324. public HashSet&lt;string&gt; SetValue { get; set; } = new HashSet&lt;string&gt;();
  325. }
  326. }
  327. </code></pre><hr>
  328. <p>I mentioned earlier that your config file will be automatically reloaded -- but isn&#39;t that a bad thing? Doesn&#39;t that mean that the config could change
  329. under your feet without you having a way to tell?</p>
  330. <p>Not so- I just haven&#39;t introduced the mechanism.</p>
  331. <p>Define a public or protected virtual method named <code>OnReload</code>:</p>
  332. <pre><code class="lang-cs" name="PluginConfig.cs#on-reload">public virtual void OnReload()
  333. {
  334. // this is called whenever the config file is reloaded from disk
  335. // use it to tell all of your systems that something has changed
  336. // this is called off of the main thread, and is not safe to interact
  337. // with Unity in
  338. }
  339. </code></pre><p>This method will be called whenever BSIPA reloads your config from disk. When it is called, the object will already have been populated. Use it to
  340. notify all of your systems that configuration has changed.</p>
  341. <hr>
  342. <p>Now, we know how to read from disk, and how to use unusual types, but how do we write it back to disk?</p>
  343. <p>This config system is based on automatic saving (though we haven&#39;t quite gotten to the <em>automatic</em> part), and so the config is written to disk whenever
  344. the system recognizes that something has changed. To tell is as much, define a public or protected virtual method named <code>Changed</code>:</p>
  345. <pre><code class="lang-cs" name="PluginConfig.cs#changed">public virtual void Changed()
  346. {
  347. // this is called whenever one of the virtual properties is changed
  348. // can be called to signal that the content has been changed
  349. }
  350. </code></pre><p>This method can be called to tell BSIPA that this config object has changed. Later, when we enable automated change tracking, this will also be called
  351. when one of the config&#39;s members changes. You can use this body to validate something or, for example, write a timestamp for last change.</p>
  352. <hr>
  353. <p>I just mentioned automated change tracking -- lets add that now.</p>
  354. <p>To do this, just make all of the properties virtual, like so:</p>
  355. <pre><code class="lang-cs" name="PluginConfig.cs#auto-props">public class SubThingsObject
  356. {
  357. public virtual double DoubleValue { get; set; } = 2.718281828459045;
  358. }
  359. public virtual int IntValue { get; set; } = 42;
  360. public virtual float FloatValue { get; set; } = 3.14159f;
  361. [NonNullable]
  362. public virtual SubThingsObject SubThings { get; set; } = new SubThingsObject();
  363. [UseConverter(typeof(ListConverter&lt;string&gt;))]
  364. public virtual List&lt;string&gt; ListValue { get; set; } = new List&lt;string&gt;();
  365. [UseConverter(typeof(CollectionConverter&lt;string, HashSet&lt;string&gt;&gt;))]
  366. public virtual HashSet&lt;string&gt; SetValue { get; set; } = new HashSet&lt;string&gt;();
  367. </code></pre><p>Now, whenever you assign to any of those properties, your <code>Changed</code> method will be called, and the config object will be marked as changed and will be
  368. written to disk. Unfortunately, any properties that can be modified while only using the property getter do not trigger this, and so if you change any
  369. collections for example, you will have to manually call <code>Changed</code>.</p>
  370. <p>After doing all this, your config class should look something like this:</p>
  371. <pre><code class="lang-cs" name="PluginConfig.cs#basic-complete">using System.Collections.Generic;
  372. using IPA.Config.Stores;
  373. using IPA.Config.Stores.Attributes;
  374. using IPA.Config.Stores.Converters;
  375. namespace Demo
  376. {
  377. public class PluginConfig
  378. {
  379. public static PluginConfig Instance { get; set; }
  380. public class SubThingsObject
  381. {
  382. public virtual double DoubleValue { get; set; } = 2.718281828459045;
  383. }
  384. public virtual int IntValue { get; set; } = 42;
  385. public virtual float FloatValue { get; set; } = 3.14159f;
  386. [NonNullable]
  387. public virtual SubThingsObject SubThings { get; set; } = new SubThingsObject();
  388. [UseConverter(typeof(ListConverter&lt;string&gt;))]
  389. public virtual List&lt;string&gt; ListValue { get; set; } = new List&lt;string&gt;();
  390. [UseConverter(typeof(CollectionConverter&lt;string, HashSet&lt;string&gt;&gt;))]
  391. public virtual HashSet&lt;string&gt; SetValue { get; set; } = new HashSet&lt;string&gt;();
  392. public virtual void Changed()
  393. {
  394. // this is called whenever one of the virtual properties is changed
  395. // can be called to signal that the content has been changed
  396. }
  397. public virtual void OnReload()
  398. {
  399. // this is called whenever the config file is reloaded from disk
  400. // use it to tell all of your systems that something has changed
  401. // this is called off of the main thread, and is not safe to interact
  402. // with Unity in
  403. }
  404. }
  405. }
  406. </code></pre><hr>
  407. <p>There is one more major problem with this though: the main class is still public. Most configs shouldn&#39;t be. Lets make it internal.</p>
  408. <p>So we make it internal:</p>
  409. <pre><code class="lang-cs" name="PluginConfig.cs#internal">internal class PluginConfig
  410. </code></pre><p>But to make it actually work, we add this outside the namespace declaration:</p>
  411. <pre><code class="lang-cs" name="PluginConfig.cs#internals-visible">using System.Runtime.CompilerServices;
  412. [assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)]
  413. </code></pre><p>And now our full file looks like this:</p>
  414. <pre><code class="lang-cs" name="PluginConfig.cs#basic-complete">using System.Collections.Generic;
  415. using System.Runtime.CompilerServices;
  416. using IPA.Config.Stores;
  417. using IPA.Config.Stores.Attributes;
  418. using IPA.Config.Stores.Converters;
  419. [assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)]
  420. namespace Demo
  421. {
  422. internal class PluginConfig
  423. {
  424. public static PluginConfig Instance { get; set; }
  425. public class SubThingsObject
  426. {
  427. public virtual double DoubleValue { get; set; } = 2.718281828459045;
  428. }
  429. public virtual int IntValue { get; set; } = 42;
  430. public virtual float FloatValue { get; set; } = 3.14159f;
  431. [NonNullable]
  432. public virtual SubThingsObject SubThings { get; set; } = new SubThingsObject();
  433. [UseConverter(typeof(ListConverter&lt;string&gt;))]
  434. public virtual List&lt;string&gt; ListValue { get; set; } = new List&lt;string&gt;();
  435. [UseConverter(typeof(CollectionConverter&lt;string, HashSet&lt;string&gt;&gt;))]
  436. public virtual HashSet&lt;string&gt; SetValue { get; set; } = new HashSet&lt;string&gt;();
  437. public virtual void Changed()
  438. {
  439. // this is called whenever one of the virtual properties is changed
  440. // can be called to signal that the content has been changed
  441. }
  442. public virtual void OnReload()
  443. {
  444. // this is called whenever the config file is reloaded from disk
  445. // use it to tell all of your systems that something has changed
  446. // this is called off of the main thread, and is not safe to interact
  447. // with Unity in
  448. }
  449. }
  450. }
  451. </code></pre></article>
  452. </div>
  453. <div class="hidden-sm col-md-2" role="complementary">
  454. <div class="sideaffix">
  455. <div class="contribution">
  456. <ul class="nav">
  457. <li>
  458. <a href="https://github.com/beat-saber-modding-group/BeatSaber-IPA-Reloaded/blob/31ecf1089859a66b6871fb0cd33e7054e996c56a/docs/articles/start-dev.md/#L1" class="contribution-link">Improve this Doc</a>
  459. </li>
  460. </ul>
  461. </div>
  462. <nav class="bs-docs-sidebar hidden-print hidden-xs hidden-sm affix" id="affix">
  463. <!-- <p><a class="back-to-top" href="#top">Back to top</a><p> -->
  464. </nav>
  465. </div>
  466. </div>
  467. </div>
  468. </div>
  469. <footer>
  470. <div class="grad-bottom"></div>
  471. <div class="footer">
  472. <div class="container">
  473. <span class="pull-right">
  474. <a href="#top">Back to top</a>
  475. </span>
  476. <span>Generated by <strong>DocFX</strong></span>
  477. </div>
  478. </div>
  479. </footer>
  480. </div>
  481. <script type="text/javascript" src="../styles/docfx.vendor.js"></script>
  482. <script type="text/javascript" src="../styles/docfx.js"></script>
  483. <script type="text/javascript" src="../styles/main.js"></script>
  484. </body>
  485. </html>