Skip to content

For Developers

Caeden117 edited this page Mar 31, 2022 · 34 revisions

Here is where you'll find tutorials and information about various development-related things for Counters+. Whether you just want to get the project set up, or you want to expand upon the project, this page should have the information you need.

Index

Because this page might be long, here are some quick links to the various ways you can contribute to Counters+.

Heck Integration

As of Counters+ 2.2.3, there is now an optional integration with Heck, the mod that powers Noodle Extensions and Chroma. Counters+ can now utilize its Settings Setter feature to allow mappers to control Counters+ on a map-per-map basis.

Unfortunately, Counters+ has way too many settings to document them all here. We even generate them dynamically; the list will change with every addition and removal from Counters+.

Category ID: _countersPlus Setting Names: _<counterName><PropertyName>

Example

{
  "_version": "2.0.0",
  "_songName": "NULCTRL MEISO FLIP",
  // ...
  "_difficultyBeatmapSets": [
    {
      "_beatmapCharacteristicName": "Standard",
      "_difficultyBeatmaps": [
        {
          "_difficulty": "ExpertPlus",
          "_difficultyRank": 9,
          "_beatmapFilename": "ExpertPlusStandard.dat",
          "_noteJumpMovementSpeed": 19,
          "_customData": {
            "_settings": {
              "_countersPlus":{
                // Main Settings
                "_mainEnabled" : true,
                "_mainHideCombo" : true,
                "_mainHideMultiplier" : true,
                "_mainParentedToBaseGameHUD" : false,
                "_mainSize" : 10,
                "_mainPositionScale" : 3,
                "_mainPosX" : 0,
                "_mainPosY" : 0,
                "_mainPosZ" : 2,
                "_mainRotX" : 45,
                "_mainRotY" : 0,
                "_mainRotZ" : 0,
                "_mainCurveRadius" : 15,
                "_mainDistanceModifier" : 1,

                // Missed settings
                "_missedEnabled" : true,
                "_missedPosition" : "AboveCombo",
                "_missedDistance" : -1,
                "_missedCountBadCuts" : true,
                "_missedCanvasID" : -1,

                // Progress settings
                "_progressEnabled" : true,
                "_progressPosition" : "AboveMultiplier",
                "_progressDistance" : -1,
                "_progressMode" : "Original",
                "_progressCanvasID" : -1,

                // Score settings
                "_scoreEnabled" : true,
                "_scorePosition" : "AboveHighway",
                "_scoreDistance" : -1,
                "_scoreMode" : "Original",
                "_scoreDecimalPrecision" : 4,
                "_scoreCustomRankColors" : true,
                "_scoreSSColor" : "#FFFFFF",
                "_scoreSColor" : "#FFFF00",
                "_scoreAColor" : "#FF00FF",
                "_scoreBColor" : "#FF0000",
                "_scoreCColor" : "#00FFFF",
                "_scoreDColor" : "#00FF00",
                "_scoreEColor" : "#0000FF",
                "_scoreCanvasID" : -1,

                // PB settings
                "_personalBestEnabled" : true,
                "_personalBestCanvasID" : -1,
                "_personalBestUnderScore" : true,
                "_personalBestDecimalPrecision" : 4,

                // We don't want additional counters, disable the rest
                "_speedEnabled" : false,
                "_cutEnabled" : false,
                "_spinometerEnabled" : false,
                "_notesLeftEnabled" : false,
                "_failEnabled" : false
              }
            }
          }
        }
      ]
    }
  ]
}

Getting Set Up

After forking or cloning the Counters+ source, there are a few extra steps you need to accomplish to get the plugin building.

Counters+ was made using Zinga's Beat Saber Modding Tools. It is highly recommended you install this plugin, as it makes mod development much easier.

To set the project up, simply right click the Counters+ project in the Solution Explorer, and navigate to Beat Saber Modding Tools >>> Set Beat Saber Directory.... Click that, and the references should automatically be set up for you.

If that does not work for whatever reason, right click the References dropdown underneath the Counters+ project in the Solution Explorer, then click Beat Saber Reference Manager. Simply uncheck a reference, then hit Apply. After that, re-check that reference, then click Apply again.

Custom Counter System

Have you ever wanted to add a counter of your own to Counters+, but you want it to also be its own standalone mod? Counters+ has a solution that allows you to easily implement any Counter of choice into the Counters+ system.

Developers who create UI Enhancement mods should look into Counters+ integration, as users may have setups that will collide with your mod.

Dependency or No Dependency?

While the Custom Counter system will always be designed such that a Counters+ dependency is optional, it will be easier on the side of the developer to have a hard Counters+ dependency. With a hard dependency in mind, you do not have to worry about maintaining two separate parts of code; one for if Counters+ is installed, and one where it's not.

Getting Started

To get started, go to your plugin's manifest.json file.

Counters+ 2.0.0's Custom Counter system uses the new BSIPA Feature's system, introduced in BSIPA versions after 4.0.5. If needed, you can grab the latest BSIPA builds here.

Here is an example Custom Counter defined in manifest.json:

"features": {
  "CountersPlus.CustomCounter": {
    "Name": "Example Custom Counter",
    "CounterLocation": "ExampleCustomCounterMod.CustomCounter",
    "MultiplayerReady": true,
    "ConfigDefaults": {
      "Enabled": true,
      "Position": "AboveCombo",
      "Distance": 0
    },
    "BSML": {
      "Resource": "ExampleCustomCounterMod.TestCustomUI.bsml",
      "Host": "ExampleCustomCounterMod.TestCustomUIHost",
      "Icon": "ExampleCustomCounterMod.william_gay.png"
    }
  }
}

If not defined already, add a features object to the manifest. Unlike previously, this new Features system uses an object, not an array. After this, you want to create a new child object under this, called CountersPlus.CustomCounter.

Name (Required)

The name of your Custom Counter. This displays in the Counters+ Settings Menu, as well as being used as an identifier for various internal components. Be warned! You should come up with a good name, because I do not recommend changing it after it's first public release!

Description

A description for your Custom Counter. Currently unused.

MultiplayerReady

Introduced in Counters+ 2.3.0, this is an opt-in flag which allows Custom Counters to load in Multiplayer.

By default, all Custom Counters are disabled in Multiplayer. This is to preserve compatibility with older Custom Counters, which might not play nicely when thrown into Multiplayer. When ready, developers can enable their Custom Counters in Multiplayer by setting this flag to true.

CounterLocation (Required)

The namespace location to the Custom Counter you want to implement. This can be a MonoBehaviour, a class that inherits a Counters+ Custom Counter type, or neither of those.

DO NOT WORRY ABOUT CREATING AN INSTANCE! Counters+ uses Zenject to new up an instance of your Custom Counter when entering a song. This also means you can take full advantage of Zenject in your Custom Counters!

ConfigDefaults

If you wish to provide your own defaults on where the counter will go, override this object. This is a ConfigModel, and is comprised of 3 parts:

  • Enabled which controls, well, whether or not it's enabled in the first place.
  • Position is an Enum of string values that act as a "relative" point to go off of. Valid options are:
    • AboveCombo and BelowCombo
    • AboveMultiplier and BelowMultiplier
    • BelowEnergy
    • AboveHighway
  • Distance is steps taken away from these relative points to determine the final position.

BSML

This is an optional object which describes behavior in the Counters+ Settings Menu. You can skip this object if you do not plan on offering config options, or a custom icon.

Resource

This is a namespace location to a .bsml file, which will be appended underneath the ConfigModel options.

For this BSML file, it is recommended to base your UI off of this template to keep consistent with Counters+:

<vertical xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/ https://raw.githubusercontent.com/monkeymanboy/BSML-Docs/gh-pages/BSMLSchema.xsd' spacing='1' horizontal-fit='PreferredSize' vertical-fit='PreferredSize'>
     <!-- BSML goes here -->
</vertical>

You may safely leave this blank; if so, no additional content (or host objects) will be appended.

Host

This is a namespace location to a Type, which will handle all UIValues, UIActions, and UIEvents for your provided BSML file.

Like with CounterLocation, Counters+ uses Zenject magic to automatically create this object when it is needed. You can also take full advantage of Zenject inside this object.

Icon

This is a namespace location to an Image, which will override the default Custom Counter icon in the list of all Counters.

I'm unsure of exactly all the supported file formats, but I'm 100% certain .png works, and about 85% certain .jpg works. I wouldn't test your luck with anything else.

Creating a Counter

If you already have a MonoBehaviour that you wish to reuse, then sure, you can plug that into the CounterLocation and Counters+ will gladly pick it up and load it in game. However, I really recommend that you create the Custom Counter from scratch to fully take advantage of all that Counters+ 2.0.0 has to offer.

Counters+ provides a few public classes, in the CountersPlus.Counters.Custom namespace, which you can inherit to speed up your custom counting.

BasicCustomCounter

This is a barebones Custom Counter class that only provides you with a few utility objects (Injected with Zenject), as well as some methods to help get you started.

CanvasUtility

CanvasUtility is a field inside this class that provides you with quick access to functions related to Counters+ canvases, as well as quick and easy text generation.

Settings

This is the CustomConfigModel that is used for your Custom Counter. You can use this, combined with CanvasUtility, to easily create custom text in just one line of code.

CounterInit

CounterInit is an abstract method (Meaning you have to override it) which is called when Counters+ loads the object. This is where you create your counter text, and subscribe to events.

CounterDestroy

CounterDestroy is an abstract method (Meaning you have to override it) which is called when the object is destroyed. This is where you clean up after yourself; unsubscribe from events, and destroy anything that might not automatically be picked up by Unity.

CanvasCustomCounter

If you really want to reuse an existing Canvas, then you can let Counters+ reposition it for you with a few additional lines of code, and a Custom Counter.

This CanvasCustomCounter class inherits BasicCustomCounter, so helpers like the Settings object and CanvasUtility will also make its way over to CanvasCustomCounter.

The CanvasCustomCounter class is designed to look for a defined Canvas object, whether by name or by direct reference, and re-parent and reposition it into the Counters+ system.

CanvasReference

This is a virtual property (Meaning overriding it is optional) which points to a direct Canvas object. You should try using this first. Remember: The Custom Counter you will be using can take full advantage of Zenject. If your mod happens to use Zenject for its own components, you can easily inject your main Canvas into the Custom Counter type.

CanvasObjectName

This is a virtual property (Meaning overriding it is optional) which gives the exact, case-sensitive name to a GameObject with an attached Canvas component. This should be your Plan B. Counters+ will give your Custom Counter 10 shots to find this Canvas object before giving up.

PreReparent

This is a virtual method (Meaning overriding it is optional) that is triggered before Counters+ attempts to search for your Canvas. This is essentially the same as CounterInit, in case you still need to do event subscribing.

PostReparent

This is a virtual method (Meaning overriding it is optional) that is triggered after Counters+ has successfully re-parented your Canvas into the Counters+ system. If there is any repositioning or manual work that still needs to be done, you can do so in the PostReparent function.

Helper Interfaces

But wait, there's more!

If you need to subscribe to basic game events, such as note cutting/missing and score updating, Counters+ has interfaces that expose these events for you, and will automatically trigger them without you needing to go out, find an object reference, and subscribing to these events yourself. One more thing, these interfaces are designed so that, in the case of a breaking game update, Counters+ will only break in one spot. This means that I would only have to fix code in one place, and these interfaces will be fully operational.

These helper interfaces are located in the CountersPlus.Counters.Interfaces namespace.

INoteEventHandler

This interface exposes basic note cut and missing events.

IScoreEventHandler

This interface exposes basic score updating, and max score updating events. Both methods only give out modified scores (As in, score when taking into account the user's modifier bonus).

Adding New Options to Existing Counters

If you feel that an existing Counters+ counter needs a new feature or option, then it might be worth forking Counters+ for yourself and looking into adding this yourself. This short section will cover the 3 main components you need to cover in order to add a new option to Counters+, complete with UI support.

We'll assume that you've already forked Counters+, got the project set up, and fixed any reference issues.

Adding to a ConfigModel

The first thing you should do when adding a new Counters+ option is to add it to an existing ConfigModel.

A ConfigModel is an abstract class located in CountersPlus.ConfigModels which holds configuration data for every counter in Counters+. Each Counter in Counters+ have an inherited ConfigModel class of their own, also located in the bottom of Config.cs. Find the ConfigModel class of the Counter you wish to add an option to, and add it, along with the default value you wish to assign. Do not add to the ConfigModel class itself, as you'll find that option for every single Counter in Counters+. Unless you want to.

The ConfigModel objects are also used as hosts in BSML, so add any BSML-related attributes and information here too.

Modifying Counter code

With your new Option in hand, go into the Counters folder of the Counters+ solution and modify the counter class you want with that new option. All of the Counters have a Settings variable, which you can easily use to access your new setting.

Settings UI

Now it's time to add your setting to the Counters+ Settings UI. For every counter, their BSML files are located in CountersPlus.UI.BSML.Config.

You can simply copy and paste code from the other BSML files to add your option to the Counters+ menu.

Pull Request

Once you're done, shoot a pull request to the master branch, and I'll look over it and see if it's good enough to merge into master.