Skip to main content

Collections

danger

Avoid using createCollection at all costs. Full explanation at the end.

A collection is just a group of data, like a list of numbers, or a dictionary of strings. MoonSharp automatically converts some commonly used C# collections into Lua tables, and vice versa.

This can be quite nice, since you can deal with collections entirely in the Lua paradigm, instead of needing to work with C# stuff directly.

For example, in this snippet:

local illuminatis = getItemsByMemeId("Illuminati") -- illuminatis is a table

the getItemsByMemeId function has been implemented on the C# side to return a List<GameObject>, which is a C# collection. Then it automatically gets converted to a Lua table in your ModScript, and now you can use Lua's ipairs to loop through it.

You should stick to Lua tables wherever possible to avoid breaking other ModScripts

However, there can be problems with these tables in some cases.

The Problem

When MoonSharp automatically converts the C# collection to a Lua table (and vice versa), it has to create a new copy of the collection instead of keeping the same reference to the old one. This is usually not a problem, but there are some cases where it is.

See, in Unity code, there are a lot of methods that take an Array, or a List, as an argument, then modify that collection within the method. I don't know if this is great practice on Unity's behalf, but it's the reality of which we must live.

So, if you try and pass a Lua table to a Unity method, it gets automatically copied to a new C# collection, and so the modification happens on that copy of the collection, not the original Lua table.

So after calling the Unity method, the Lua table will remain unchanged, which is probably not what you want. For example, this wouldn't work:

local audioSource = getItemsByMemeId("HeNeedsSomeMilk")[1].GetComponent("AudioSource")
local audioSamples = {} -- here we define a lua table
audioSource.clip.GetData(audioSamples, 0); -- here we pass that table to a Unity method that expects an array
print(#audioSamples) -- this will be 0, because the Unity method didn't modify the original table

It might correctly convert the audioSamples table to the C# array and populate that with audio sample data within GetData, but the original audioSamples table will be unchanged.

The Solution

To get around this problem, I made a createCollection helper function that creates a C# collection internally, then wraps it in MoonSharp's UserData type before passing it back to the ModScript. This prevents it from being automatically converted to a new Lua table, and allows a direct reference to that same exact collection to be used from within your ModScript,

So createCollection returns the UserData object it created internally, and now you can keep a direct reference to it from your Lua ModScript, and pass that to the Unity method that modifies the collection instead.

createCollection takes as arguments:

  1. The fully qualified type name of the collection, e.g.
    • "System.Single[]" for an Array of floats,
    • "System.Collections.Generic.List`1[[UnityEngine.ContactPoint2D, UnityEngine]]" for a List of type UnityEngine.ContactPoint2D
    • "System.Collections.Generic.Dictionary`2[[System.String], [Enemy, Assembly-CSharp]]" for a Dictionary where the key type is System.String, and value type is Enemy (which is a YOMG2 type, so we use the Assembly-CSharp assembly)
  2. A key-value Lua table of options. Right now it's used for arrays only, i.e. { arrayLength = 666 }

Here's how you could use it:

registerType("AudioSource")
registerType("AudioClip")

local audioSource = getItemsByMemeId("HeNeedsSomeMilk")[1].GetComponent("AudioSource") -- needs a "HeNeedsSomeMilk" item in the level
local samplesCount = audioSource.clip.samples * audioSource.clip.channels
local samples = createCollection("System.Single[]", {arrayLength = samplesCount}) -- arrays are a special case from other collections that don't take generic arguments, and have a fixed, known size

audioSource.clip.GetData(samples, 0);

for i = 0, samples.Length - 1 do
local randomFloat = 0.5 + (math.random() * 0.5) -- rand between 0.5 and 1
samples[i] = samples[i] * randomFloat -- can use square brackets to index C# collections!
end

audioSource.clip.SetData(samples, 0); -- ruins the audio, this is just an example

Remember, C# collections used 0-based indexing, and Lua tables are 1-based, so in a C# collection the first element will be at index 0.

danger

The reason to not use createCollection: after you do this once, MoonSharp will no longer automatically convert a collection of a specified type into a Lua table. Instead it will just return a direct reference to the C# collection object. And that happens to all ModScript code everywhere that executes after createCollection was called, even if it's not related. So it could interfere with any other ModScript in the level, potentially breaking it.

Why?

Because createCollection needs to (interally) call registerType with the provided type, and registering types is global. You could just call registerType and pass the C# collection type yourself at the very start, if you want more consistent behaviour from the get-go.

Or better you could set the execution order of your scripts with createCollection to something higher than everything else, so it runs after and doesn't affect them, but this is not foolproof and will likely cause issues for other people who want to use your ModScripts in their levels.

It is a bit annoying, but there's not much I can do about it. At least it probably won't be a problem most of the time - I don't know why you'd need to create a collection of GameObjects for any Unity methods (which would cause the return type of getItemsByMemeId to change to List). Coz remember, this only affects the specific type of collection you are creating.