Skip to content

Intro to Lodestone Atom

Peter Jiang edited this page Jun 17, 2024 · 3 revisions

⚠️ WARNING: Lodestone Atom is in beta, so

  • The API is not stable
  • Breaking changes can be made at any time without warning
  • The documentation is not complete
  • Code may have unintended side effects

⚠️ WARNING: No further development will be made on Lodestone Atom in favour of Docker Instance.

Last updated for: v0.5.0-beta.3

Prerequisites

  • Intermediate TypeScript knowledge (async/await, OOP, etc)
  • Familiarity with Deno
  • Familiarity with Lodestone on a user level

What is Lodestone Atom?

Lodestone Atom allows you to implement your custom instance logic in typescript.

For those familiar with Pterodactyl eggs, Lodestone Atom serves a similar purpose, but with typescript instead of a declarative json.

You can host the code of your Atom on Github, and import it via a url.

Setup

  1. Macros are executed in a modified Deno runtime, so it is recommended that you follow their environment setup.

Note that this is not strictly necessary, but will provide you with a better developer experience.

Throughout this guide, we will be using VSCode, but you can use any editor you like as long as it supports the Deno language server.

  1. Download Lodestone CLI.

Install the latest Lodestone Core beta with

lodestone_cli -v v0.5.0-beta.3

and run it

  1. Head over to https://dev.lodestone.cc/ and connect to your core

Getting started

Since the architecture of Lodestone Atom is quite complicated, let's start with a example.

In the instance creation screen, paste in the following url for Atom: https://raw.githubusercontent.com/Lodestone-Team/lodestone-atom-lib/main/examples/basic.ts

Then click "Load Instance", and click through the rest of the creation process, the setting fields don't matter.

You should see a progress bar to indicate the instance is being created.

Once it's done, click on the instance power button and click "Start", it will then put "Output" in the instance console every second.

You can also type in commands in the console, and it will respond with "Got command, {your command}".

Anatomy of an Atom

An Atom is a typescript class that extends the Atom class from the lodestone-atom-lib package.

Let's break down the example Atom method by method.

    public async setupManifest(): Promise<Atom.SetupManifest> {
        return {
            setting_sections: {
                "section_id1": {
                    section_id: "section_id1",
                    name: "section_name1",
                    description: "section_description1",
                    settings: {
                        "setting_id1": {
                            setting_id: "setting_id1",
                            name: "Port",
                            description: "Port to run the server on",
                            value: null,
                            value_type: { type: "UnsignedInteger", min: 0, max: 65535 },
                            default_value: { type: "UnsignedInteger", value: 6969 },
                            is_secret: false,
                            is_required: true,
                            is_mutable: true,
                        }
                    },
                }
            }
        };
    }

The setupManifest method is used to define the settings that the user can configure when creating an instance.

You should recall that we were able to set the port of the instance in the creation screen.

The setting_sections field is a map of SettingSections, which are used to group settings together.

Note that all section_ids should be unique, and all setting_ids should be unique across all sections.

    public async setup(setupValue: Atom.SetupValue, dotLodestoneConfig: Atom.DotLodestoneConfig, progression_handler: ProgressionHandler, path: string): Promise<void> {
        this.uuid = dotLodestoneConfig.uuid;
        let port: number;
        if (setupValue.setting_sections["section_id1"].settings["setting_id1"].value?.type == "UnsignedInteger") {
            port = setupValue.setting_sections["section_id1"].settings["setting_id1"].value.value;
        } else {
            throw new Error("Invalid value type");
        }
        this.config = {
            name: setupValue.name,
            description: setupValue.description ?? "",
            port: port,
        };

        // write config to file
        await Deno.writeTextFile(path + "/" + TestInstance.restoreConfigName, JSON.stringify(this.config));

        this.event_stream = new EventStream(this.uuid, this.config.name);

        for (let i = 0; i < 100; i++) {
            progression_handler.setProgress(i, `Progress ${i}`);
            await new Promise(r => setTimeout(r, 100));
        }

        // no need to call `progression_handler.complete()` since it's handled.
        return;
    }

The setup method is the "constructor" of the Atom.

The setupValue parameter contains the values that the user has configured in the creation screen, and the dotLodestoneConfig parameter contains values generated by Lodestone, such as the instance uuid, time of creation, etc.

The path parameter is the path to the instance directory, all of your files should be stored in this directory.

The progression_handler parameter is used to report progress to Lodestone, which will be displayed in the notification area and instance list.

Usually you will have to call complete() on the progression handler, but the setup method is a special case, where Lodestone will automatically call complete() when the method returns. You can still call complete() early if you want to.

We also write the config to a file, so that we can restore it later.

    public async restore(dotLodestoneConfig: Atom.DotLodestoneConfig, path: string): Promise<void> {
        this.uuid = dotLodestoneConfig.uuid;
        this.event_stream = new EventStream(this.uuid, this.config.name);
        this.config = JSON.parse(await Deno.readTextFile(path + "/" + TestInstance.restoreConfigName)) as RestoreConfig;
        return;
    }

The restore method is called when the Atom is restored from file system by Lodestone.

Note the lack of state passed into the parameters, the implementation is responsible for restoring any state the Atom wishes to persist, this is why we wrote the config to file in the setup method.

    public async start(caused_by: Atom.CausedBy, block: boolean): Promise<void> {
        console.log("start");
        this.event_stream.emitStateChange("Running");
        this._state = "Running";
        (async () => {
            while (this._state == "Running") {
                await new Promise(r => setTimeout(r, 1000));
                this.event_stream.emitConsoleOut("Output");
            }
        })();
        return;
    }

The start method is called when the instance is requested to start.

The caused_by parameter is planned to be deprecated, so don't worry about it.

The block parameter is used to indicate whether the method should block until the instance is started.

If block is set to false, then this method should perform the bare minimum to check for valid states and start the instance, then return as soon as possible.

If block is set to true, then the method should block until the instance is fully started, and return only when the instance is ready to be used. This is useful to implement restart.

    public async stop(caused_by: Atom.CausedBy, block: boolean): Promise<void> {
        console.log("stop");
        this._state = "Stopped";
        this.event_stream.emitStateChange("Stopped");
        return;
    }

The stop method is called when the instance is requested to stop.

Just like the start method, the block parameter is used to indicate whether the method should block until the instance is fully stopped.

Clone this wiki locally