diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 4875a77..675b804 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,3 +1,9 @@
+<<<<<<< v2
+# Set update schedule for GitHub Actions
+
+version: 2
+updates:
+=======
version: 2
updates:
- package-ecosystem: "github-actions"
@@ -9,13 +15,19 @@ updates:
master-actions:
patterns:
- "*"
+>>>>>>> master
- package-ecosystem: "github-actions"
directory: "/"
schedule:
+<<<<<<< v2
+ # Check for updates to GitHub Actions every week
+ interval: "daily"
+=======
interval: "weekly"
target-branch: "docs/master"
groups:
docs-actions:
patterns:
- "*"
+>>>>>>> master
diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml
index 3752e3d..90f6d91 100644
--- a/.github/workflows/build_artifacts.yml
+++ b/.github/workflows/build_artifacts.yml
@@ -12,28 +12,41 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ submodules: true
- name: Set up JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: false
+ - name: Store short commit hash
+ run: echo "short_commit_hash=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV"
+
- name: Build with Gradle
run: ./gradlew build
env:
+ PRESERVE_PRERELEASE_VERSION: true
DISABLE_PROPERTIES_UPDATE: true
+ VERSION_SUFFIX: ${{ env.short_commit_hash }}
- - name: Delete common libs
- run: rm -r ./common/build/libs
+ - name: Publish to Maven
+ run: ./gradlew publishMavenPublicationToOffsetMonkey538Repository
+ env:
+ PRESERVE_PRERELEASE_VERSION: true
+ DISABLE_PROPERTIES_UPDATE: true
+ VERSION_SUFFIX: ${{ env.short_commit_hash }}
+ MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
- name: Artifacts
- path: ./*/build/libs/
+ name: Build Artifacts
+ path: ./loader/*/*/build/libs/
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 90e8b34..4333922 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -14,26 +14,33 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ submodules: true
- name: Set up JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: false
- name: Build with Gradle
run: ./gradlew build
env:
+ IS_RELEASE: true
DISABLE_PROPERTIES_UPDATE: true
+ VERSION_SUFFIX: ""
- name: Upload to Modrinth
run: ./gradlew modrinth
env:
+ IS_RELEASE: true
+ DISABLE_PROPERTIES_UPDATE: true
+ VERSION_SUFFIX: ""
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
VERSION_NAME: ${{ github.event.release.name }}
VERSION_IS_PRERELEASE: ${{ github.event.release.prerelease }}
@@ -42,13 +49,13 @@ jobs:
- name: Publish to Maven
run: ./gradlew publishMavenPublicationToOffsetMonkey538Repository
env:
+ IS_RELEASE: true
+ DISABLE_PROPERTIES_UPDATE: true
+ VERSION_SUFFIX: ""
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- - name: Delete common libs
- run: rm -r ./common/build/libs
-
- name: Upload to GitHub
uses: softprops/action-gh-release@v2
with:
- files: ./*/build/libs/*.jar
+ files: ./loader/*/*/build/libs/*.jar
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..cc7fe46
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "buildSrc"]
+ path = buildSrc
+ url = https://github.com/OffsetMods538/multiversion-buildscripts
diff --git a/LICENSE b/LICENSE
index 9e03ee7..6b9c8b3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2024,2025 OffsetMonkey538
+Copyright (c) 2024-2026 OffsetMonkey538
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index efd06c5..a3b7c63 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ This class has to implement the `HttpHandler` interface, this will look somethin
public class MyHttpHandler implements HttpHandler {
@Override
- public void handleRequest(@NotNull ChannelHandlerContext ctx, @NotNull FullHttpRequest request) throws Exception {
+ public void handleRequest(@NonNull ChannelHandlerContext ctx, @NonNull FullHttpRequest request) throws Exception {
// Logic will go here
}
}
@@ -64,7 +64,7 @@ public class MyHttpHandler implements HttpHandler {
Now we'll need to actually implement the handler. You can google "HTTP Netty" for more info on how to handle HTTP requests with Netty.
```java
-public void handleRequest(@NotNull ChannelHandlerContext ctx, @NotNull FullHttpRequest request) throws Exception {
+public void handleRequest(@NonNull ChannelHandlerContext ctx, @NonNull FullHttpRequest request) throws Exception {
// Write "Hello, World!" to a buffer, encoded in UTF-8
final ByteBuf content = Unpooled.copiedBuffer("Hello, World!", StandardCharsets.UTF_8);
// Create a response with said buffer
diff --git a/build.gradle b/build.gradle
index 3fc62e3..221c435 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,101 +1,73 @@
-import dex.plugins.outlet.v2.util.ReleaseType
-
plugins {
- id 'fabric-loom' version '1.9-SNAPSHOT' apply false
- id 'io.github.dexman545.outlet' version '1.6.1' apply false
- id 'com.modrinth.minotaur' version '2.+' apply false
- id 'maven-publish'
-}
-
-ext {
- debugVersion = System.currentTimeMillis()
+ id 'multiloader-root'
+ id 'java'
+ id 'java-library'
+ id 'net.neoforged.moddev' version "${neoforged_moddev}" apply false
+ id 'fabric-loom' version "${fabric_loom}" apply false
+ id 'io.papermc.paperweight.userdev' version "${papermc_paperweight_userdev}" apply false
+ id 'xyz.jpenilla.run-paper' version "${jpenilla_run_task}" apply false
+ id 'com.gradleup.shadow' version "${gradleup_shadow}" apply false
+ id 'xyz.jpenilla.resource-factory' version "${jpenilla_resource_factory}" apply false
+ id 'io.github.dexman545.outlet' version "${dexman_outlet}" apply false
+ id 'com.modrinth.minotaur' version "${modrinth_minotaur}" apply false
+ id 'hamburg.janove.elevator-music' version "0.1"
}
-allprojects {
- group = "top.offsetmonkey538.meshlib"
-}
+//elevatorMusic {
+// if (Boolean.parseBoolean(System.getenv("DISABLE_MUSIC"))) return
+// waitMusic = file("${rootProject.projectDir}/veryImportant/music.wav")
+// successSound = file("${rootProject.projectDir}/veryImportant/done.wav")
+//}
subprojects {
- apply plugin: "maven-publish"
- apply plugin: "java"
- apply plugin: "java-library"
-
- archivesBaseName = "mesh-lib-${project.nameSuffix}"
- version = "${rootProject.mod_version}+${rootProject.minecraft_version}"
-
- tasks.named("javadoc", Javadoc) {
- options.addFileOption('-add-stylesheet', rootProject.file("javadoc-stylesheet.css"))
- }
-
- java {
- withSourcesJar()
- withJavadocJar()
- }
-
- jar {
- from("${rootProject.projectDir}/LICENSE") {
- rename { "${it}" }
- }
- }
-
- dependencies {
- compileOnly "org.jetbrains:annotations:24.0.0"
- compileOnly "org.slf4j:slf4j-api:2.0.16"
- }
-
- publishing {
- repositories {
- maven {
- name = "OffsetMonkey538"
- url = "https://maven.offsetmonkey538.top/releases"
- credentials {
- username = providers.gradleProperty("OffsetMonkey538Username").getOrElse(System.getenv("MAVEN_USERNAME"))
- password = providers.gradleProperty("OffsetMonkey538Password").getOrElse(System.getenv("MAVEN_PASSWORD"))
- }
- authentication {
- basic(BasicAuthentication)
- }
- }
- }
- publications {
- maven(MavenPublication) {
- artifactId = project.archivesBaseName
-
- from(components["java"])
- }
- }
- }
- tasks.publishMavenPublicationToMavenLocal.doLast {
- if (System.getenv("IS_DEBUG") == "true") System.out.println("Version: " + version)
- }
+ apply plugin: "java-library"
+
+ repositories {
+ mavenCentral()
+ mavenLocal()
+ exclusiveContent {
+ forRepository {
+ maven {
+ name = "OffsetMods538"
+ url = "https://maven.offsetmonkey538.top/releases"
+ }
+ }
+ filter {
+ includeGroupAndSubgroups "top.offsetmonkey538"
+ }
+ }
+ }
+
+ dependencies {
+ compileOnlyApi "org.jspecify:jspecify:${rootProject.jspecify_version}"
+ }
}
-configure(subprojects.findAll { it.name != "common" }) {
- apply plugin: 'com.modrinth.minotaur'
- apply plugin: 'io.github.dexman545.outlet'
-
- if (System.getenv("IS_DEBUG") == "true") version = "${version}-${rootProject.debugVersion}"
-
- outlet {
- mcVersionRange = rootProject.supported_minecraft_versions
- allowedReleaseTypes = Set.of(ReleaseType.RELEASE)
- }
-
- modrinth {
- token = System.getenv("MODRINTH_TOKEN")
- projectId = "mesh-lib"
- def customVersionName = System.getenv("VERSION_NAME")
- if (customVersionName != null) versionName = customVersionName
- versionNumber = "${project.version}"
- versionType = "alpha"
- def isPreRelease = System.getenv("VERSION_IS_PRERELEASE")
- versionType = !"false".equals(isPreRelease) ? "beta" : "release"
- additionalFiles = [sourcesJar.archiveFile]
- gameVersions = outlet.mcVersions()
- syncBodyFrom = rootProject.file("README.md").text
- def changelogEnv = System.getenv("VERSION_CHANGELOG")
- if (changelogEnv != null) changelog = changelogEnv
- }
-
- tasks.modrinth.dependsOn(tasks.modrinthSyncBody)
+configure(subprojects.findAll { it.hasProperty("project_name") }) {
+ apply plugin: "maven-publish"
+
+ publishing {
+ repositories {
+ maven {
+ name = "OffsetMonkey538"
+ url = "https://maven.offsetmonkey538.top/releases"
+ credentials {
+ username = providers.gradleProperty("OffsetMonkey538Username").getOrElse(System.getenv("MAVEN_USERNAME"))
+ password = providers.gradleProperty("OffsetMonkey538Password").getOrElse(System.getenv("MAVEN_PASSWORD"))
+ }
+ authentication {
+ basic(BasicAuthentication)
+ }
+ }
+ }
+ publications {
+ register("maven", MavenPublication) {
+ afterEvaluate {
+ artifactId base.archivesName.get()
+ }
+ groupId "top.offsetmonkey538.meshlib"
+ from components.java
+ }
+ }
+ }
}
diff --git a/buildSrc b/buildSrc
new file mode 160000
index 0000000..31a2373
--- /dev/null
+++ b/buildSrc
@@ -0,0 +1 @@
+Subproject commit 31a2373f007a4ef7aaaabdcf4d28fd283ef42175
diff --git a/common/build.gradle b/common/build.gradle
index 84516a6..1034bef 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -1,14 +1,25 @@
plugins {
- id 'com.gradleup.shadow' version '9.0.0-beta4'
- id 'maven-publish'
+ id 'multiloader-base'
}
repositories {
- mavenCentral()
+ maven {
+ name = "Mojang Libraries"
+ url = "https://libraries.minecraft.net"
+ content {
+ includeGroup "com.mojang"
+ }
+ }
}
dependencies {
- // Netty
- api "io.netty:netty-codec-http:${project.netty_version}"
+ compileOnlyApi("top.offsetmonkey538.monkeylib538:monkeylib538-common:${rootProject.monkeylib538_version}+common") {
+ exclude(group: "net.kyori")
+ }
+ compileOnlyApi "io.netty:netty-codec-http:${rootProject.netty_version}"
+ compileOnlyApi "io.netty:netty-transport-classes-epoll:${rootProject.netty_version}"
+ compileOnly "com.google.guava:guava:${rootProject.guava_version}"
+ compileOnly "com.mojang:brigadier:${rootProject.brigadier_version}"
+
+ compileOnlyApi "net.kyori:adventure-api:${project.adventure_api_version}"
}
-tasks.build.dependsOn(shadowJar)
diff --git a/common/gradle.properties b/common/gradle.properties
index 9e33a89..6d3b82c 100644
--- a/common/gradle.properties
+++ b/common/gradle.properties
@@ -1,5 +1,5 @@
-## Netty, only used for netty-codec-http. Hopefully this version won't conflict with other versions of Netty
-netty_version = 4.1.82.Final
+project_name = common
-
-nameSuffix = api
+# Dependencies
+## Adventure
+adventure_api_version = 4.26.1
\ No newline at end of file
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/MESHLib.java b/common/src/main/java/top/offsetmonkey538/meshlib/MESHLib.java
deleted file mode 100644
index 0316b26..0000000
--- a/common/src/main/java/top/offsetmonkey538/meshlib/MESHLib.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package top.offsetmonkey538.meshlib;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Just used to store some constants
- */
-public final class MESHLib {
- /**
- * Private constructor as this class shouldn't be instanced
- */
- private MESHLib() {}
-
- /**
- * String modid for this mod
- */
- public static final String MOD_ID = "mesh-lib";
- /**
- * Logger instance used by this mod
- */
- public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
-}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandler.java
deleted file mode 100644
index c6aa42e..0000000
--- a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandler.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package top.offsetmonkey538.meshlib.api;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelFutureListener;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.http.DefaultFullHttpResponse;
-import io.netty.handler.codec.http.FullHttpRequest;
-import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.netty.util.CharsetUtil;
-import top.offsetmonkey538.meshlib.example.SimpleHttpHandler;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
-import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
-import static top.offsetmonkey538.meshlib.MESHLib.LOGGER;
-
-/**
- * An http handler for you to implement :D
- *
- * This class also provides some utility methods for making your life easier.
- *
- * For example {@link #sendError(ChannelHandlerContext, HttpResponseStatus)} {@link #sendError(ChannelHandlerContext, HttpResponseStatus, String)}
- *
- * Look at {@link SimpleHttpHandler SimpleHttpHandler} for an example
- *
- * @see HttpHandlerRegistry
- */
-@FunctionalInterface
-public interface HttpHandler {
-
- /**
- * This is called when an HTTP request is received for this handler.
- *
- *
- *
- * @param ctx the current channel handler context
- * @param request the received request
- * @throws Exception when anything goes wrong
- */
- void handleRequest(@NotNull ChannelHandlerContext ctx, @NotNull FullHttpRequest request) throws Exception;
-
- /**
- * Sends the requester an error code.
- *
- * @param ctx the current channel handler context
- * @param status the received request
- * @see #sendError(ChannelHandlerContext, HttpResponseStatus, String)
- */
- static void sendError(@NotNull ChannelHandlerContext ctx, @NotNull HttpResponseStatus status) {
- sendError(ctx, status, null);
- }
-
- /**
- * Sends the requester an error code and optionally a reason with it.
- *
- * @param ctx the current channel handler context
- * @param status the received request
- * @param reason reason to display for the error, may be null or empty
- * @see #sendError(ChannelHandlerContext, HttpResponseStatus)
- */
- static void sendError(@NotNull ChannelHandlerContext ctx, @NotNull HttpResponseStatus status, @Nullable String reason) {
- final String message = String.format("Failure: %s\r\n%s",status, (reason == null || reason.isBlank() ? "" : "Reason: " + reason + "\r\n"));
-
- LOGGER.error(message);
-
- final ByteBuf byteBuf = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8);
- final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, byteBuf);
-
- response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
- ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
- }
-}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandlerRegistry.java b/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandlerRegistry.java
deleted file mode 100644
index 7c05d3c..0000000
--- a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandlerRegistry.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package top.offsetmonkey538.meshlib.api;
-
-import org.jetbrains.annotations.NotNull;
-import top.offsetmonkey538.meshlib.impl.HttpHandlerRegistryImpl;
-
-/**
- * Registry for the handlers
- *
- * Each handler will only be able to listen to requests on *its* sub-path.
- *
- * If your handler's id is {@code testmod}, then your handler will only receive requests on {@code server.com/testmod/}
- * @see HttpHandler
- */
-public interface HttpHandlerRegistry {
- /**
- * Instance
- */
- HttpHandlerRegistry INSTANCE = new HttpHandlerRegistryImpl();
-
- /**
- * Method for registering a {@link HttpHandler}
- *
- * THE ID SHOULD NOT BE EMPTY
- *
- * @param id your handler or mod's id
- * @param handler the {@link HttpHandler} to be registered
- * @throws IllegalArgumentException when the provided id is empty or a handler with this id is already registered
- * @see HttpHandler
- */
- void register(@NotNull String id, @NotNull HttpHandler handler) throws IllegalArgumentException;
-
- /**
- * Method for getting a registered {@link HttpHandler}
- *
- * @param id the handler's id
- * @return the {@link HttpHandler} for the provided id
- * @throws IllegalStateException when there is no {@link HttpHandler} registered for the provided id
- */
- @NotNull
- HttpHandler get(@NotNull String id) throws IllegalStateException;
-
- /**
- * Returns true if handler with provided id is registered, false otherwise
- *
- * @param id the id to check
- * @return true if handler with provided id is registered, false otherwise
- */
- boolean has(@NotNull String id);
-}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/MESHLib.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/MESHLib.java
new file mode 100644
index 0000000..fc841be
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/MESHLib.java
@@ -0,0 +1,86 @@
+package top.offsetmonkey538.meshlib.common;
+
+import top.offsetmonkey538.meshlib.common.api.MESHLibApi;
+import top.offsetmonkey538.meshlib.common.api.example.ExampleMain;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticContentHandler;
+import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticDirectoryHandler;
+import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticFileHandler;
+import top.offsetmonkey538.meshlib.common.api.router.HttpRouter;
+import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.rules.DomainHttpRule;
+import top.offsetmonkey538.meshlib.common.api.rule.rules.PathHttpRule;
+import top.offsetmonkey538.meshlib.common.config.MESHLibConfig;
+import top.offsetmonkey538.meshlib.common.config.RouterConfigHandler;
+import top.offsetmonkey538.meshlib.common.netty.NettyServer;
+import top.offsetmonkey538.meshlib.common.platform.PlatformUtil;
+import top.offsetmonkey538.monkeylib538.common.api.command.CommandRegistrationApi;
+import top.offsetmonkey538.monkeylib538.common.api.command.ConfigCommandApi;
+import top.offsetmonkey538.monkeylib538.common.api.lifecycle.ServerLifecycleApi;
+import top.offsetmonkey538.monkeylib538.common.api.telemetry.TelemetryRegistry;
+import top.offsetmonkey538.offsetutils538.api.config.ConfigHolder;
+import top.offsetmonkey538.offsetutils538.api.config.ConfigManager;
+import top.offsetmonkey538.offsetutils538.api.config.event.JanksonConfigurationEvent;
+import top.offsetmonkey538.offsetutils538.api.log.OffsetLogger;
+
+import java.util.ServiceLoader;
+
+public final class MESHLib {
+ /**
+ * Private constructor as this class shouldn't be instanced
+ */
+ private MESHLib() {}
+
+ /**
+ * String modid for this mod
+ */
+ public static final String MOD_ID = "meshlib";
+ /**
+ * Logger instance used by this mod
+ */
+ public static final OffsetLogger LOGGER = OffsetLogger.create(MOD_ID);
+
+ public static final ConfigHolder CONFIG = ConfigManager.init(ConfigHolder.create(MESHLibConfig::new, LOGGER));
+
+
+ public static void initialize() {
+ TelemetryRegistry.register(MOD_ID);
+
+ PlatformUtil.enableVanillaHandler();
+ ExampleMain.onInitialize();
+
+ ConfigCommandApi.registerConfigCommand(CONFIG, () -> {
+ MESHLibApi.reload();
+ MESHLibApi.initialize();
+ }, MOD_ID, "config");
+ CommandRegistrationApi.registerCommand(RouterConfigHandler.createExampleConfigCommand());
+
+ JanksonConfigurationEvent.JANKSON_CONFIGURATION_EVENT.listen(HttpRouter::configureJankson);
+
+ HttpRuleTypeRegistry.HTTP_RULE_REGISTRATION_EVENT.listen(DomainHttpRule::register);
+ HttpRuleTypeRegistry.HTTP_RULE_REGISTRATION_EVENT.listen(PathHttpRule::register);
+
+ HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(StaticContentHandler::register);
+ HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(StaticFileHandler::register);
+ HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(StaticDirectoryHandler::register);
+
+ HttpRouterRegistry.HTTP_ROUTER_REGISTRATION_EVENT.listen(RouterConfigHandler::init);
+
+ ServerLifecycleApi.STARTING.listen(MESHLibApi::reload);
+ ServerLifecycleApi.STARTED.listen(MESHLibApi::initialize);
+ }
+
+ public static void disableAllHandlers() {
+ PlatformUtil.disableVanillaHandler();
+ NettyServer.stop();
+ }
+
+
+ public static T load(Class clazz) {
+ LOGGER.info("Loading service for: %s", clazz);
+ return ServiceLoader.load(clazz, MESHLib.class.getClassLoader())
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("Failed to load service for " + clazz.getName()));
+ }
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/MESHLibApi.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/MESHLibApi.java
new file mode 100644
index 0000000..6801643
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/MESHLibApi.java
@@ -0,0 +1,42 @@
+package top.offsetmonkey538.meshlib.common.api;
+
+import org.jspecify.annotations.Nullable;
+import top.offsetmonkey538.meshlib.common.impl.MESHLibApiImpl;
+import top.offsetmonkey538.offsetutils538.api.annotation.Internal;
+
+/**
+ * Api for interacting with mesh lib
+ */
+public interface MESHLibApi {
+ @Internal
+ MESHLibApi INSTANCE = new MESHLibApiImpl();
+
+ /**
+ * Reloads MESH Lib.
+ */
+ static void reload() {
+ INSTANCE.reloadImpl();
+ }
+
+ /**
+ * Initializes.
+ */
+ static void initialize() {
+ INSTANCE.initializeImpl();
+ }
+
+ /**
+ * Provides the external port as defined in the MESH Lib config.
+ *
{@code null} value indicates that MESH Lib has not been set up correctly and isn't running.
+ *
+ * @return the port the server is accessible from externally
+ */
+ static @Nullable Integer getExternalPort() {
+ return INSTANCE.getExternalPortImpl();
+ }
+
+
+ @Internal void reloadImpl();
+ @Internal void initializeImpl();
+ @Internal @Nullable Integer getExternalPortImpl();
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleHttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleHttpHandler.java
new file mode 100644
index 0000000..7d5f3e5
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleHttpHandler.java
@@ -0,0 +1,49 @@
+package top.offsetmonkey538.meshlib.common.api.example;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRule;
+import top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil;
+import top.offsetmonkey538.offsetutils538.api.annotation.Internal;
+
+/**
+ * An example {@link HttpHandler} implementation to learn from
+ */
+public record ExampleHttpHandler(String baseContent) implements HttpHandler {
+
+ @Override
+ public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception {
+ // Calculate response using super amazing and hard math™
+ final String responseText = superCoolMethodForRunningTheHardAndAmazingCalculationForCalculationinatingTheResponseTM(request.uri());
+
+ // Magical utility for sending a plain-text string
+ HttpResponseUtil.sendString(ctx, request, responseText);
+ }
+
+ private String superCoolMethodForRunningTheHardAndAmazingCalculationForCalculationinatingTheResponseTM(String requestUri) {
+ final String requestedPath = requestUri.substring(requestUri.indexOf('/'));
+
+ return this.baseContent + requestedPath;
+ }
+
+ public static void register(final HttpHandlerTypeRegistry registry) {
+ registry.register("example-http", Data.class, ExampleHttpHandler.class, handler -> new Data(handler.baseContent), data -> new ExampleHttpHandler(data.content));
+ }
+
+ @Internal
+ private static final class Data {
+ private String content;
+
+ @SuppressWarnings("unused")
+ // Pretty sure this public no-args needs to exist cause jankson wants to create instances
+ public Data() {
+
+ }
+
+ public Data(final String content) {
+ this.content = content;
+ }
+ }
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleMain.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleMain.java
new file mode 100644
index 0000000..a47045b
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleMain.java
@@ -0,0 +1,40 @@
+package top.offsetmonkey538.meshlib.common.api.example;
+
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.router.HttpRouter;
+import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.rules.DomainHttpRule;
+
+import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER;
+
+/**
+ * Initializer for the example handlers
+ */
+public final class ExampleMain {
+ private ExampleMain() {
+
+ }
+
+ /**
+ * Initializer for example handlers
+ *
+ * Checks if the {@code meshEnableExamples} system property is enabled and registers the example handlers if so
+ */
+ public static void onInitialize() {
+ // Ignore if "meshEnableExamples" isn't set
+ if (!Boolean.getBoolean("meshEnableExamples")) return;
+
+
+ LOGGER.warn("MESH examples enabled!");
+
+ // Register
+ HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(ExampleHttpHandler::register);
+
+ HttpRouterRegistry.HTTP_ROUTER_REGISTRATION_EVENT.listen(registry -> {
+ registry.register("example/example-http-handler", new HttpRouter(
+ new DomainHttpRule("site.example.com"),
+ new ExampleHttpHandler("Requested path: ")
+ ));
+ });
+ }
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/package-info.java
new file mode 100644
index 0000000..d266916
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * This contains examples for how to implement {@link top.offsetmonkey538.meshlib.common.api.handler.HttpHandler HttpHandler}s
+ */
+@NullMarked
+package top.offsetmonkey538.meshlib.common.api.example;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandler.java
new file mode 100644
index 0000000..27c916f
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandler.java
@@ -0,0 +1,29 @@
+package top.offsetmonkey538.meshlib.common.api.handler;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import top.offsetmonkey538.meshlib.common.api.example.ExampleHttpHandler;
+import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRule;
+
+/**
+ * An http handler for you to implement :D
+ *
+ * Look at {@link ExampleHttpHandler ExampleHttpHandler} for an example
+ *
+ * @see HttpRouterRegistry
+ */
+public interface HttpHandler {
+
+ /**
+ * This is called when an HTTP request is received for this handler.
+ *
+ *
+ *
+ * @param ctx the current channel handler context
+ * @param request the received request
+ * @param rule the rule used to match this handler
+ * @throws Exception when anything goes wrong
+ */
+ void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception;
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandlerTypeRegistry.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandlerTypeRegistry.java
new file mode 100644
index 0000000..123f713
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandlerTypeRegistry.java
@@ -0,0 +1,72 @@
+package top.offsetmonkey538.meshlib.common.api.handler;
+
+import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry;
+import top.offsetmonkey538.meshlib.common.impl.router.HttpHandlerTypeRegistryImpl;
+import top.offsetmonkey538.offsetutils538.api.annotation.Internal;
+import top.offsetmonkey538.offsetutils538.api.event.Event;
+
+import java.util.function.Function;
+
+/**
+ * Registry for {@link HttpHandler}s, use the {@link #HTTP_HANDLER_REGISTRATION_EVENT} event for registering your handlers.
+ */
+public interface HttpHandlerTypeRegistry {
+
+ /**
+ * Instance
+ */
+ @Internal
+ HttpHandlerTypeRegistry INSTANCE = new HttpHandlerTypeRegistryImpl();
+
+ /**
+ * Internal method for clearing the registry, no touch!
+ */
+ @Internal
+ static void clear() {
+ INSTANCE.clearImpl();
+ }
+
+ void clearImpl();
+ void register(final String type, final Class dataType, final Class handlerType, final Function handlerToData, final Function dataToHandler);
+ HttpHandlerTypeRegistryImpl.HttpHandlerDefinition,?> get(final String type) throws IllegalArgumentException;
+ HttpHandlerTypeRegistryImpl.HttpHandlerDefinition,?> get(final Class extends HttpHandler> type) throws IllegalArgumentException;
+
+
+ /**
+ * Event for registering http handlers.
+ *
+ * The registry is cleared upon reloading, so to make your handlers persist, you need to register them in this event.
+ *
+ *
+ * Initially called while the server is starting, so make sure to register your handler before that!
+ *
+ *
+ * Called before the {@link HttpRouterRegistry#HTTP_ROUTER_REGISTRATION_EVENT HTTP_ROUTER_REGISTRATION_EVENT} event.
+ *
+ */
+ Event HTTP_HANDLER_REGISTRATION_EVENT = Event.createEvent(HttpHandlerRegistrationEvent.class, handlers -> registry -> {
+ for (HttpHandlerRegistrationEvent handler : handlers) handler.register(registry);
+ });
+
+ /**
+ * Handler for {@link #HTTP_HANDLER_REGISTRATION_EVENT}.
+ */
+ @FunctionalInterface
+ interface HttpHandlerRegistrationEvent {
+
+ /**
+ * Internal method for invoking the event without providing the registry, no touch!
+ */
+ @Internal
+ default void invoke() {
+ register(INSTANCE);
+ }
+
+ /**
+ * Registers {@link HttpHandler}s to the provided registry
+ *
+ * @param registry the registry to register to
+ */
+ void register(final HttpHandlerTypeRegistry registry);
+ }
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticContentHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticContentHandler.java
new file mode 100644
index 0000000..5bd64ab
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticContentHandler.java
@@ -0,0 +1,37 @@
+package top.offsetmonkey538.meshlib.common.api.handler.handlers;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRule;
+import top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil;
+import top.offsetmonkey538.offsetutils538.api.annotation.Internal;
+
+public record StaticContentHandler(String content) implements HttpHandler {
+
+ @Override
+ public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception {
+ HttpResponseUtil.sendString(ctx, request, content);
+ }
+
+ @Internal
+ public static void register(final HttpHandlerTypeRegistry registry) {
+ registry.register("static-content", Data.class, StaticContentHandler.class, handler -> new Data(handler.content), data -> new StaticContentHandler(data.content));
+ }
+
+ @Internal
+ private static final class Data {
+ private String content = "";
+
+ @SuppressWarnings("unused")
+ // Pretty sure this public no-args needs to exist cause jankson wants to create instances
+ public Data() {
+
+ }
+
+ public Data(final String content) {
+ this.content = content;
+ }
+ }
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticDirectoryHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticDirectoryHandler.java
new file mode 100644
index 0000000..dd09757
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticDirectoryHandler.java
@@ -0,0 +1,200 @@
+package top.offsetmonkey538.meshlib.common.api.handler.handlers;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.LastHttpContent;
+import io.netty.util.CharsetUtil;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRule;
+import top.offsetmonkey538.offsetutils538.api.annotation.Internal;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER;
+import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendError;
+import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendFile;
+import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendPermanentRedirect;
+
+public record StaticDirectoryHandler(Path baseDir, boolean allowDirectoryList) implements HttpHandler {
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss");
+
+ public StaticDirectoryHandler(final Path baseDir, final boolean allowDirectoryList) {
+ this.baseDir = baseDir.normalize().toAbsolutePath();
+ this.allowDirectoryList = allowDirectoryList;
+ }
+
+ @Override
+ public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception {
+ final String rawPath = new URI(rule.normalizeUri(request.uri())).getPath();
+ final Path requestedPath;
+ try {
+ requestedPath = baseDir.resolve(rawPath.startsWith("/") ? rawPath.substring(1) : rawPath).normalize();
+ } catch (InvalidPathException e) {
+ sendError(ctx, request, HttpResponseStatus.BAD_REQUEST, e);
+ return;
+ }
+
+ if (!requestedPath.startsWith(baseDir)) {
+ sendError(ctx, request, HttpResponseStatus.FORBIDDEN);
+ return;
+ }
+
+ if (!Files.exists(requestedPath)) {
+ sendError(ctx, request, HttpResponseStatus.NOT_FOUND);
+ return;
+ }
+
+ if (!Files.isDirectory(requestedPath)) {
+ sendFile(ctx, request, requestedPath);
+ return;
+ }
+
+ // At this point we know it's a directory and that it exists
+ if (!request.uri().endsWith("/")) {
+ sendPermanentRedirect(ctx, request, request.uri() + "/");
+ return;
+ }
+
+ if (allowDirectoryList) {
+ sendDirectoryListing(ctx, request, rawPath, requestedPath);
+ return;
+ }
+
+ // Try serving an index.html file
+ sendFile(ctx, request, requestedPath.resolve("index.html"));
+ }
+
+ private static void sendDirectoryListing(ChannelHandlerContext ctx, FullHttpRequest request, String uriPath, Path directory) throws IOException {
+ final boolean isKeepAlive = HttpUtil.isKeepAlive(request);
+
+ final ByteBuf byteBuf = Unpooled.copiedBuffer(renderDirectoryListing(uriPath, directory), CharsetUtil.UTF_8);
+ final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.OK, byteBuf);
+
+ response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
+ response.headers().set(CONTENT_LENGTH, byteBuf.readableBytes());
+ response.headers().set(CONNECTION, isKeepAlive ? KEEP_ALIVE : CLOSE);
+
+
+ ctx.write(response).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
+
+ final ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
+ if (!isKeepAlive) lastContentFuture.addListener(ChannelFutureListener.CLOSE);
+ }
+
+ private static StringBuilder renderDirectoryListing(String uriPath, Path directory) throws IOException {
+ final StringBuilder result = new StringBuilder("Index of ").append(uriPath).append("
Index of ");
+ result.append(uriPath);
+ result.append("
Name
Last Modified
Size
");
+
+ if (!"/".equals(uriPath)) {
+ result.append("
");
+
+ return result;
+ }
+
+ private static void formatFileSize(StringBuilder builder, Path path) {
+ if (Files.isDirectory(path)) {
+ builder.append("-");
+ return;
+ }
+
+ final long sizeBytes;
+ try {
+ sizeBytes = Files.size(path);
+ } catch (IOException e) {
+ LOGGER.error("Failed to get size for file '%s'!", e, path);
+ builder.append("-");
+ return;
+ }
+
+ // https://stackoverflow.com/a/3758880
+ long absB = sizeBytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(sizeBytes);
+ if (absB < 1024) {
+ builder.append(sizeBytes).append(" B");
+ return;
+ }
+
+ long value = absB;
+ final CharacterIterator charIterator = new StringCharacterIterator("KMGTPE");
+ for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) {
+ value >>= 10;
+ charIterator.next();
+ }
+ value *= Long.signum(sizeBytes);
+ builder.append("%.1f %ciB".formatted(value / 1024.0, charIterator.current()));
+ }
+
+ @Internal
+ public static void register(final HttpHandlerTypeRegistry registry) {
+ registry.register("static-directory", Data.class, StaticDirectoryHandler.class, handler -> new Data(handler.baseDir, handler.allowDirectoryList), data -> new StaticDirectoryHandler(Path.of(data.baseDir), data.allowDirectoryList));
+ }
+
+ @Internal
+ private static final class Data {
+ private String baseDir = "";
+ private boolean allowDirectoryList;
+
+ // Pretty sure this public no-args needs to exist cause jankson wants to create instances
+ @SuppressWarnings("unused")
+ public Data() {
+
+ }
+
+ public Data(final Path baseDir, final boolean allowDirectoryList) {
+ this.baseDir = baseDir.toString();
+ this.allowDirectoryList = allowDirectoryList;
+ }
+ }
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticFileHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticFileHandler.java
new file mode 100644
index 0000000..94ae267
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticFileHandler.java
@@ -0,0 +1,42 @@
+package top.offsetmonkey538.meshlib.common.api.handler.handlers;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRule;
+import top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil;
+import top.offsetmonkey538.offsetutils538.api.annotation.Internal;
+
+import java.nio.file.Path;
+
+public record StaticFileHandler(Path fileToServe) implements HttpHandler {
+ public StaticFileHandler(final Path fileToServe) {
+ this.fileToServe = fileToServe.normalize().toAbsolutePath();
+ }
+
+ @Override
+ public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception {
+ HttpResponseUtil.sendFile(ctx, request, fileToServe);
+ }
+
+ @Internal
+ public static void register(final HttpHandlerTypeRegistry registry) {
+ registry.register("static-file", Data.class, StaticFileHandler.class, handler -> new Data(handler.fileToServe), data -> new StaticFileHandler(Path.of(data.fileToServe)));
+ }
+
+ @Internal
+ private static final class Data {
+ private String fileToServe;
+
+ @SuppressWarnings("unused")
+ // Pretty sure this public no-args needs to exist cause jankson wants to create instances
+ public Data() {
+
+ }
+
+ public Data(final Path fileToServe) {
+ this.fileToServe = fileToServe.toString();
+ }
+ }
+}
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/package-info.java
new file mode 100644
index 0000000..bc73d92
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Built-in {@link top.offsetmonkey538.meshlib.common.api.handler.HttpHandler HttpHandler}s
+ */
+@NullMarked
+package top.offsetmonkey538.meshlib.common.api.handler.handlers;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/package-info.java
new file mode 100644
index 0000000..faa0955
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Http handler
+ */
+@NullMarked
+package top.offsetmonkey538.meshlib.common.api.handler;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/api/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/package-info.java
similarity index 74%
rename from common/src/main/java/top/offsetmonkey538/meshlib/api/package-info.java
rename to common/src/main/java/top/offsetmonkey538/meshlib/common/api/package-info.java
index 9e511da..f34abe6 100644
--- a/common/src/main/java/top/offsetmonkey538/meshlib/api/package-info.java
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/package-info.java
@@ -3,4 +3,4 @@
*
* There should be no need to interact with anything outside of this package
*/
-package top.offsetmonkey538.meshlib.api;
\ No newline at end of file
+package top.offsetmonkey538.meshlib.common.api;
\ No newline at end of file
diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouter.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouter.java
new file mode 100644
index 0000000..8b94029
--- /dev/null
+++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouter.java
@@ -0,0 +1,82 @@
+package top.offsetmonkey538.meshlib.common.api.router;
+
+import blue.endless.jankson.Jankson;
+import blue.endless.jankson.JsonObject;
+import blue.endless.jankson.JsonPrimitive;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler;
+import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRule;
+import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry;
+import top.offsetmonkey538.meshlib.common.impl.router.HttpHandlerTypeRegistryImpl;
+import top.offsetmonkey538.meshlib.common.impl.router.rule.HttpRuleTypeRegistryImpl;
+
+public record HttpRouter(HttpRule rule, HttpHandler handler) {
+
+ /**
+ * Configures the provided {@link Jankson.Builder} with serializers and deserializers for {@link HttpRule}s and {@link HttpHandler}s.
+ *
+ * When using my own config library, OffsetUtils538, its {@link top.offsetmonkey538.offsetutils538.api.config.event.JanksonConfigurationEvent#JANKSON_CONFIGURATION_EVENT JANKSON_CONFIGURATION_EVENT} will have this configurator registered already and there's no need to call this method.
+ *
+ * For other config libraries... idk try to understand whatever the fuck I'm doing in this method I guess.....
+ *
+ *
+ * @param janksonBuilder the builder to configure
+ * @return the builder instance
+ */
+ public static Jankson.Builder configureJankson(final Jankson.Builder janksonBuilder) {
+ janksonBuilder.registerSerializer(HttpRule.class, (httpRule, marshaller) -> {
+ @SuppressWarnings({"unchecked"})
+ // rule definition of ?,? extends HttpRule should match ?,HttpHandler, no?
+ final HttpRuleTypeRegistryImpl.HttpRuleDefinition, HttpRule> ruleDefinition = (HttpRuleTypeRegistryImpl.HttpRuleDefinition, HttpRule>) ((HttpRuleTypeRegistryImpl) HttpRuleTypeRegistry.INSTANCE).get(httpRule.getClass());
+
+ final JsonObject result = (JsonObject) marshaller.serialize(ruleDefinition.ruleToData().apply(httpRule));
+ result.put("type", JsonPrimitive.of(ruleDefinition.type()));
+ return result;
+ });
+
+ janksonBuilder.registerDeserializer(JsonObject.class, HttpRule.class, (jsonObject, marshaller) -> {
+ final String type = jsonObject.get(String.class, "type");
+ if (type == null) throw new RuntimeException("HttpRule doesn't contain 'type' field!");
+
+ @SuppressWarnings({"unchecked"}) // It's proooobably a subclass of Object...
+ final HttpRuleTypeRegistryImpl.HttpRuleDefinition