Docs / CT v4.3.0 / module development

Build a runnable module.

A CT feature module is one jar with three parts: a ct.module.json manifest, a class that implements CtModule, and an installer entry point that copies the jar into the modules folder when someone runs it. This page builds one from an empty folder, installs it into CT-Main, and shows what the main program gains.

Build tools JDK 21 or later, Maven 3.9+ (or plain javac) Result ct-module-<id>-<version>.jar, runnable and loadable Source code.html

What a module is

CT-Main is a bare host. It scans a modules folder on every start, reads each jar's ct.module.json, sorts the modules by the ids they require, and loads each one in its own class loader. A loaded module can register views, publish services, and provide commands; a module whose requirements are missing is skipped with a reason, and CT-Main keeps running with the rest.

The jar has to do two different jobs, and that is the part most people get wrong:

  • As a module it is loaded by CT-Main, which already provides the platform API classes. The module jar should not contain them.
  • As a file it is run by a person, straight after download, with nothing else on the classpath. It has to install itself, so it needs a Main-Class and a copy of the installer code inside.

Both jobs are satisfied by the packaging shown below: the module classes stay theirs, and only the small installer package from the platform is baked in.

1. Project layout

One folder per module. This example builds a module with the id hello that adds a view to CT-Main.

ct-module-hello/
├── pom.xml
└── src/main/
    ├── java/com/example/hello/
    │   ├── HelloModule.java
    │   └── HelloPanel.java
    └── resources/ct.module.json

2. The pom

The module compiles against ct-main for the platform API. The shade plugin packs exactly two things into the finished jar: the module's own classes, and the com.ct.main.moduleinstall package from ct-main — everything else from the platform, and every transitive dependency, is filtered out. The manifest transformer sets the entry point that makes the file runnable.

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>ct-module-hello</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <ct.version>4.3.0</ct.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.ct</groupId>
      <artifactId>ct-main</artifactId>
      <version>${ct.version}</version>
      <exclusions>
        <exclusion>
          <groupId>com.google.code.gson</groupId>
          <artifactId>gson</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.6.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>shade</goal></goals>
            <configuration>
              <createDependencyReducedPom>false</createDependencyReducedPom>
              <artifactSet>
                <includes><include>com.ct:ct-main</include></includes>
              </artifactSet>
              <filters>
                <filter>
                  <artifact>com.ct:ct-main</artifact>
                  <includes><include>com/ct/main/moduleinstall/**</include></includes>
                </filter>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>com.ct.main.moduleinstall.ModuleInstaller</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Why the exclusion? Without it, Gson rides in transitively from ct-main and every module jar grows by a few hundred kilobytes of a library the host already provides. Add the library back with compile scope only if your module must run outside the host, the way the ctloader module does inside the game process.

Coming from the CT source tree? The parent pom.xml already carries this configuration for every ct-module-* module; a new module in the reactor only declares the maven-shade-plugin and inherits it.

3. ct.module.json

The manifest is what CT-Main reads before it loads anything. It lives at the root of the jar.

{
  "schemaVersion": 1,
  "id": "hello",
  "version": "1.0.0",
  "name": "Hello Module",
  "description": "Adds a hello view to CT-Main.",
  "requires": ["theme"],
  "entrypoint": "com.example.hello.HelloModule"
}
FieldRequiredMeaning
schemaVersionyesManifest format version; 1.
idyesModule identity. Lowercase, matches [a-z][a-z0-9_-]{0,63}, unique in the modules folder.
versionnoYour build's version. The installer uses it for the installed file name and to replace older copies; it falls back to the file name.
namenoDisplay name in the console and the Modules view. Defaults to the id.
descriptionnoOne line shown next to the module.
requiresnoModule ids that must load first. The module is skipped, with a reason, when one is missing or failed.
entrypointyesFully qualified class implementing com.ct.main.api.CtModule with a public no-argument constructor.

4. The module class

CtModule.initialize is called once, after every module it requires has loaded. Everything the host offers arrives through ModuleContext.

package com.example.hello;

import com.ct.main.api.CtModule;
import com.ct.main.api.ModuleContext;

public final class HelloModule implements CtModule {
    @Override
    public void initialize(ModuleContext context) {
        context.registerView("Hello", new HelloPanel(context));
        context.console().writeLine("Hello module ready.");
    }
}

HelloPanel is an ordinary JComponent. If you require the theme module you can look its service up and use the CT widgets; otherwise a plain panel keeps your module independent.

package com.example.hello;

import com.ct.main.api.ModuleContext;
import java.awt.BorderLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;

public final class HelloPanel extends JPanel {
    public HelloPanel(ModuleContext context) {
        setLayout(new BorderLayout());
        add(new JLabel("Hello from " + context.mainVersion()), BorderLayout.CENTER);
    }
}

The whole API in one line each: registerView(title, component) adds a sidebar entry; console().writeLine(text) writes to the console view; publishService(type, instance) and service(type) connect modules to each other; accounts() reads the selected account; applicationDirectory(), modulesDirectory(), and the settings module's store give you paths and settings; implement CommandProvider as well to answer a ct command line verb. The API page has the full reference.

5. Build the runnable jar

mvn package

In the CT source tree use the wrapper instead — ./mvnw package — and the jar appears at ct-module-hello/target/ct-module-hello-1.0.0.jar. Check what you produced:

unzip -l target/ct-module-hello-1.0.0.jar | grep -E 'ct.module.json|moduleinstall|com/example'
unzip -p target/ct-module-hello-1.0.0.jar META-INF/MANIFEST.MF | grep Main-Class

You want exactly three kinds of entry: your own classes, ct.module.json, and the com/ct/main/moduleinstall/ classes — with Main-Class: com.ct.main.moduleinstall.ModuleInstaller in the manifest. If you see com/ct/main/api or com/google/gson in there, the filters in the pom are not applied.

6. Run it — the module installs itself

Run the jar with no arguments. It copies itself into the modules folder CT-Main uses and prints what it found:

java -jar target/ct-module-hello-1.0.0.jar
CT module installer
  module   Hello Module (hello 1.0.0)
  source   /home/you/ct-module-hello/target/ct-module-hello-1.0.0.jar
  modules  /home/you/.ct-client/modules (recorded by CT-Main)
  installed ct-module-hello-1.0.0.jar (24118 bytes)

Modules in the folder (5): hello, installer, settings, theme, versions
Every module this one requires is installed.

Start CT-Main and it loads this module automatically:
  java -jar CT-Main-4.3.0.jar

The folder is resolved in this order, and the choice is printed with the result:

  1. 1

    --modules-dir <path>

    Explicit always wins.

  2. 2

    CT_MODULES_DIR

    The environment variable, for scripted installs.

  3. 3

    A modules folder next to the jar or next to a CT-Main jar

    So a module downloaded into the same folder as CT-Main lands where that launcher reads.

  4. 4

    The folder CT-Main recorded

    Every CT-Main start writes the folder it used to ~/.ct-client/last-modules-dir. This is what makes a module downloaded to any other folder still land in your launcher's folder.

  5. 5

    ~/.ct-client/modules

    The default when CT-Main has never run.

Other options: --print reports the target folder without writing, --remove uninstalls the module, --launch starts the CT-Main jar it found, --no-gui never opens the result window, --help lists everything.

On a desktop, running the jar with no console — a double-click — shows the same report in a window instead of printing it.

Or drop it onto CT-Main

CT-Main accepts module jars as arguments, which is exactly what a file manager sends when you drag files onto it. Drop one module jar, several at once, or a folder holding them onto CT-Main-4.3.0.jar and every module found is installed into the folder that copy of CT-Main reads, before it scans for modules — so the window that opens already has them:

java -jar CT-Main-4.3.0.jar target/ct-module-hello-1.0.0.jar
Installing dropped modules into /home/you/.ct-client/modules
  installed ct-module-hello-1.0.0.jar — Hello Module (hello 1.0.0) (24118 bytes)
Installed 1 module(s).
...
Loaded 2 module(s), skipped 0.
  module hello 1.0.0 (Hello Module)

Paths that are not module jars, and paths that do not exist, are reported and skipped; a path that is a folder is searched for module jars. If a launcher is already open on the same modules folder, the dropped copy installs the modules, reports what it did, and stops — close that window and start CT-Main again to use them.

7. What CT-Main gains

Start CT-Main again — or, if you dropped the jar onto it, watch the window that just opened. Nothing else to configure:

java -jar CT-Main-4.3.0.jar
CT-Main 4.3.0 — modules folder: /home/you/.ct-client/modules
Loaded 6 module(s), skipped 0.
  module cli 1.0.0 (Command Line)
  module hello 1.0.0 (Hello Module)
  ...
  • The console lists the module with its id, version, and name.
  • The sidebar gets one entry per registered view — in this example a Hello tab.
  • The Modules view lists it with its description, and shows the modules folder path with an open-folder button.
  • If the module publishes a service, every module loaded after it can look that service up by type.
  • If it provides a CommandProvider, java -jar CT-Main.jar <command> answers to it, and it is listed when no module handles the typed command.

Nothing else to register. Installing the jar is the whole integration. Add a module jar and the feature appears; delete it with --remove and the feature is gone on the next start, with everything else untouched.

8. Verify it without a display

Two smoke flags make a module testable on a build machine. The first loads the modules and prints the report, including skip reasons; the second renders every registered view offscreen to PNG files, so you can look at your view without opening a window.

java -Dct.smoke=modules -jar CT-Main-4.3.0.jar
java -Dct.smoke.render=/tmp/views.png -jar CT-Main-4.3.0.jar

Point either one at your test folder with --modules-dir <path> to check a build before installing it.

9. Without Maven

A module jar is a zip with a manifest. The slow way works too — compile against a CT-Main jar, unpack the installer package from it, and write the manifest yourself:

javac --release 21 -cp CT-Main-4.3.0.jar -d classes $(find src/main/java -name '*.java')
mkdir -p staging
cp -r classes/. staging/
cp src/main/resources/ct.module.json staging/
cd staging && unzip -o ../CT-Main-4.3.0.jar 'com/ct/main/moduleinstall/*' && cd ..
jar --create --file ct-module-hello-1.0.0.jar \
    --main-class com.ct.main.moduleinstall.ModuleInstaller \
    -C staging .

Keep com/ct/main/api out of the staging folder: the host supplies it, and a stale copy inside a module jar is a good way to get a NoSuchMethodError after a platform update.

10. When it does not load

Console saysCause and fix
skipped hello: requires missing module 'theme'A required id is not installed. Run that module's jar once, or drop requires if the dependency is optional.
skipped hello: duplicate module id; keeping ct-module-hello-1.0.0.jar …Two jars carry the same id. Delete the older one, or run the newer jar: the installer removes the older copy for you.
Module jar is missing ct.module.jsonThe manifest did not make it into the jar. Check that it is under src/main/resources, at the root of the output.
Module manifest is invalid / Invalid module idThe JSON is malformed, or the id breaks the lowercase rule. Ids are [a-z][a-z0-9_-]{0,63}.
Module entrypoint must implement com.ct.main.api.CtModuleentrypoint names the wrong class, or the class does not implement CtModule.
skipped hello: initialization failed: …The constructor or initialize threw. The message carries the exception; fix it and start CT-Main again.
no main manifest attribute when running the jarThe jar is not runnable: the manifest transformer is missing from the pom, or the shade execution did not run.

11. Publishing your module

A module needs no registry. To make it downloadable from this site, put the jar in HTML/downloads/modules/ under the name ct-module-<id>-<version>.jar and add it to the module catalog in HTML/assets/site.js:

{ id: "hello", name: "Hello Module", requires: ["theme"],
  description: "Adds a hello view to CT-Main." }

The catalog table, the download links, the module count, and the search index entry all come from that one array. Someone else then installs your feature exactly the way this page describes: download, run once, restart CT-Main.