Skip to content

Conversation

squigglesdev
Copy link
Contributor

@squigglesdev squigglesdev commented May 3, 2025

Not very far along in the process, but opening up a draft PR because I'm sure everyone has opinions on how it should work.
(Note: when I say "will", I mean "in the current implementation, they will" 🙃)

Mods will be separate solutions that depend on DLSModdingAPI, DLS, DLS.Description, and UnityEngine.CoreModule, and will export to .dls files (cosmetic .dll files, because why not?)

The basic "main" file of a mod will implement DLSModdingAPI.IMod:

namespace DLS.ModdingAPI
{
    public interface IMod
    {
        string Name { get; }
        string Version { get; }
        void Initialize();
        void OnProjectLoad();
    }
}

And thus can provide functionality when the sim is initialized (such as changing UI or other configurations), and when a project is loaded (such as registering a new builtin chip).

Here is a very basic mod:

using DLS.ModdingAPI;

namespace ExampleMod
{
    public class Mod : IMod
    {
        public string Name => "ExampleMod";

        public string Version => "0.0.1";

        public void Initialize()
        {
            Debug.Log($"{Name} v{Version} has been initialized!");
        }

        public void OnProjectLoad()
        {
            ChipCreator.RegisterChip("This little chippy went to market", new(5.25f, 0.3f), new(1, 1, 1));
            ChipCreator.RegisterChip("This little chippy stayed home", new(5.25f, 0.3f), new(0, 0, 0));
        }
    }
}

Which results in:
image

Please do pipe up with any suggestions - or how I'm doing this completely wrong - modding (especially) is a community effort!

@RobMayer
Copy link

RobMayer commented May 4, 2025

My only suggestion would be to think about dependencies - how you signal that a mod depends on another mod, how mod loading is ordered about those dependencies, and what would happen when two mods require the same third mod, but in two different versions - as early as possible.

@squigglesdev
Copy link
Contributor Author

Yeah, dependencies are currently TOTALLY unhandled haha. (I'm pretty sure the game crashes if there are no mods present, so very WIP lol)

@RobMayer
Copy link

RobMayer commented May 4, 2025

ope!

yeah, totally understandable to not have accounted for everything yet >.< - I was more suggesting that you might find that proper support for dependencies will probably inform a good chunk of your implementation, so, might be a good idea to code with that at the back of your head.

@squigglesdev
Copy link
Contributor Author

squigglesdev commented May 4, 2025

I have no idea where to even start with that, to be honest 😅

@RobMayer
Copy link

RobMayer commented May 4, 2025

I'm not sure how to do it specifically in the context of unity/c#, but I can offer some help in the abstract (I'm primarily a web-dev, so I'm out of my depth on specific implementation here, but if this were typescript... >.<)

@squigglesdev
Copy link
Contributor Author

I'm not sure how to do it specifically in the context of unity/c#, but I can offer some help in the abstract (I'm primarily a web-dev, so I'm out of my depth on specific implementation here, but if this were typescript... >.<)

I fear C# is the unholy mix of Java and TS... (or the other way around?)
But yeah any help would be great!

@squigglesdev
Copy link
Contributor Author

squigglesdev commented May 4, 2025

Quite a big update; modded chips can provide functionality:

Registry.RegisterChip (
    "PASS",
    new(CalculateGridSnappedWidth(DrawSettings.GridSize * 8), DrawSettings.GridSize * 3),
    new(1, 1, 1),
    [ModdedChipCreator.CreatePinDescription("Input", 0)],
    [ModdedChipCreator.CreatePinDescription("Output", 1)],
    [],
    false,
    simulationFunction: (inputs, outputs) =>
    {
        outputs[0].State = inputs[0].State;
    }
);

Registry.RegisterChip(
    "CONST",
    new(CalculateGridSnappedWidth(DrawSettings.GridSize * 8), DrawSettings.GridSize * 3),
    new(1, 1, 1),
    [],
    [ModdedChipCreator.CreatePinDescription("Output", 0)],
    [],
    false,
    simulationFunction: (inputs, outputs) =>
    {
        outputs[0].State = 1;
    }
);

image

I think my next goal is to allow mods to define collections to store their modded chips in :)

known issue: collections appear to duplicate on reload
@BenBE
Copy link

BenBE commented May 5, 2025

I think we need the following interfaces available for mods to use:

  • The DLS instance, allowing access to general information like version, loaded mods, directory/path information.

  • Chip Registry, including registration and modification of chips and collections.

  • Dialog API, allowing to create settings dialogs for the mod (global settings), as well as for registered chips (chip-specific configuration)

  • Runtime API, allow for registration to receive various events, like placing/removing a wire, placing/removing a chip, editing a chip or wire, handling user clicks / keyboard input, project load/save/unload.

  • Furthermore mods that influence the behaviour of other components¹ should be able to store additional data into save files. Storing additional data through the MOD should allow to specify some "requires this MOD" flag, indicating if the mod is required to load things or if the additional data is optional.

That's my rough idea what the MOD API should provide. Basically: If you had MOD building a command line the API should allow the MOD to implement a full interface to control everything going on.

¹ Think of a mod that enhances LEDs to provide not just a color, but a set of textures the user could choose from. Such a MOD would need a way to store the selected textures in the save file and retrieve it later on load. Loading such a project should gracefully notify the user about any MODs required for proper handling.

@sk337
Copy link

sk337 commented May 5, 2025

Please manifests and create a JSON Schema for the manifest of your do so one of my biggest gripes w/ terraria’s modding system is the build.txt manifest if you could make a schema based manifest I would love that and an update system integrated through GitHub releases so a mod could have the repo linked and the updater would look for a file formatted like {modIdent}-{version}-{platform}-{arch}.{dynamicObjectExt}

@squigglesdev
Copy link
Contributor Author

I've created a Trello:
https://trello.com/invite/b/68187bc79d8ef26a6e36f386/ATTId95f9716b6095482e3785c277073091077909438/dls-moddingapi

That way we can properly decide on what to add (and what's currently being worked on)

I've already added some of @BenBE's great suggestions.

@squigglesdev
Copy link
Contributor Author

Just discovered a pretty big issue - IL2CPP (obviously) doesn't support loading assemblies at runtime.
Would we be okay with switching back to Mono? Unfortunate for performance...

@sk337
Copy link

sk337 commented May 5, 2025

Just discovered a pretty big issue - IL2CPP (obviously) doesn't support loading assemblies at runtime. Would we be okay with switching back to Mono? Unfortunate for performance...

terraria uses il2cpp modding is never impossible just share symbols and use normal LoadLibrary with a custom entry point

@squigglesdev
Copy link
Contributor Author

squigglesdev commented May 5, 2025

That would require people to write mods in C++. (And thus require a complete rewrite of the API)

@sk337
Copy link

sk337 commented May 5, 2025

That would require people to write mods in C++. (And thus require a complete rewrite of the API)

in this case i would take the loss since we are not super far into the modding process and players who dont mod are just gonna get hit with a performance downgrade if a switch to mono occurs. so i think that the better solution would be to go with a c++ modding api

@squigglesdev
Copy link
Contributor Author

Just did some research, Terraria doesn't use Unity and therefore doesn't use IL2CPP. Terraria mods are written in C# so I'm not entirely sure what you mean.

@sk337
Copy link

sk337 commented May 5, 2025

Just did some research, Terraria doesn't use Unity and therefore doesn't use IL2CPP. Terraria mods are written in C# so I'm not entirely sure what you mean.

2am brain (idk either)

(sadly IL2CPP no longer supported)
@squigglesdev
Copy link
Contributor Author

Experimenting with embedding the mono runtime in the game to dynamically load mods... seems excessive haha

@squigglesdev
Copy link
Contributor Author

I've gotten nowhere on that. Just going to stick with Mono builds for now whilst I add to the API.

(unless we want to interpret a totally different language like lua or something 🙃)

@squigglesdev
Copy link
Contributor Author

I've got displays working!

Here is a gorgeous example (note sold separately):
image

And for reference, here is the MOD code used to create it:

using DLS.Game;
using DLS.Graphics;
using DLS.ModdingAPI;
using Seb.Vis;
using UnityEngine;

namespace ExampleMod
{
    public class Mod : IMod
    {
        public string Name => "ExampleMod";

        public string Version => "0.0.1";

        public void Initialize()
        {
            var passChip = new ChipBuilder("PASS")
                .SetSize(new(CalculateGridSnappedWidth(DrawSettings.GridSize * 8), DrawSettings.GridSize * 3))
                .SetColor(new(1, 1, 1))
                .SetInputs([new("Input", 0, Vector2.zero, PinBitCount.Bit8, PinColour.Red, PinValueDisplayMode.Off)])
                .SetOutputs([new("Output", 1)])
                .SetSimulationFunction((inputs, outputs) =>
                {
                    outputs[0] = inputs[0];
                });

            var constChip = new ChipBuilder("CONST")
                .SetSize(new(CalculateGridSnappedWidth(DrawSettings.GridSize * 8), DrawSettings.GridSize * 3))
                .SetColor(new(0.25f, 0.25f, 0.25f))
                .SetOutputs([new("Output", 1)])
                .SetSimulationFunction((inputs, outputs) =>
                {
                    outputs[0] = 1;
                });

            var ledChip = new ChipBuilder("HUGE-LED")
                .SetSize(new(CalculateGridSnappedWidth(DrawSettings.GridSize * 12), CalculateGridSnappedWidth(DrawSettings.GridSize * 12)))
                .SetColor(new(0.25f, 0.25f, 0.25f))
                .SetInputs([
                    new("Red", 0, Vector2.zero, PinBitCount.Bit8, PinColour.Red, PinValueDisplayMode.Off),
                    new("Green", 1, Vector2.zero, PinBitCount.Bit8, PinColour.Red, PinValueDisplayMode.Off),
                    new("Blue", 2, Vector2.zero, PinBitCount.Bit8, PinColour.Red, PinValueDisplayMode.Off)
                ])
                .SetDisplays([
                    new DisplayBuilder()
                        .SetPosition(Vector2.right * DrawSettings.PinRadius / 3 * 0)
                        .SetScale(DrawSettings.GridSize * 10)
                        .SetDrawFunction((pos, scale, inputs, bounds) => {
                            Draw.Quad(pos, Vector2.one * scale, Color.black);
                            Draw.Quad(pos, Vector2.one * (scale * 0.975f), new(((float)inputs[0])/255, ((float)inputs[1])/255, ((float)inputs[2])/255));
                        })
                ]);

            Registry.RegisterChips(passChip, constChip, ledChip);

            Registry.RegisterCollections(
                new CollectionBuilder("ExampleMod")
                    .AddChip(passChip)
                    .AddChip(constChip)
                    .AddChip(ledChip)
            );

            Debug.Log($"{Name} v{Version} has been initialized!");
        }


        static float CalculateGridSnappedWidth(float desiredWidth) =>
            // Calculate width such that spacing between an input and output pin on chip will align with grid
            GridHelper.SnapToGridForceEven(desiredWidth) - (DrawSettings.ChipOutlineWidth - 2 * DrawSettings.SubChipPinInset);
    }
}

@squigglesdev
Copy link
Contributor Author

Does anyone have any good ideas on how the menu/dialog builder should be implemented? internally it's handled quite differently to chips, collections, displays, etc. haha

@BraveCaperCat2
Copy link

Does anyone have any good ideas on how the menu/dialog builder should be implemented? internally it's handled quite differently to chips, collections, displays, etc. haha

Make it be handled like chips, collections and displays, maybe?

Chips containing modded subchips will now be "soft" hidden from the library if the mod they depend on is removed.
They return when the mod is loaded again.
Todo: warning message when this occurs
Untested: modded supchips several chips deep
@sk337
Copy link

sk337 commented May 8, 2025

I would recommend adding a from mod is the subchips section so an example might be

{
  "Name":"Skip",
  "Namespace": "Modname",
  "ID":1854753101,
  "Label":null,
  "Position":{
    "x":-0.14,
    "y":-2.25
  },
  "OutputPinColourInfo":[{"PinColour":0,"PinID":977613743}],
  "InternalData":null
}

or

{
  "Name":"Modname::Skip",
  "ID":1854753101,
  "Label":null,
  "Position":{
    "x":-0.14,
    "y":-2.25
  },
  "OutputPinColourInfo":[{"PinColour":0,"PinID":977613743}],
  "InternalData":null
}

this would be important to prevent crashes

@squigglesdev
Copy link
Contributor Author

@sk337 yep, thats what the last two commits were for ☺️

@sk337
Copy link

sk337 commented May 8, 2025

looking at the commit names with that context makes A LOT more sense

@squigglesdev
Copy link
Contributor Author

squigglesdev commented May 8, 2025

some of them have descriptions too 🙃

@sk337
Copy link

sk337 commented May 12, 2025

made a draft for a manifest schema

manifestSchema.json

i would be fine with making any changes you need

@squigglesdev
Copy link
Contributor Author

Looks great, haven't setup the mod dependency and update system yet though 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants