Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div align="center">

![license](https://img.shields.io/badge/license-AGPL--3.0-blue?style=flat-square)
![version](https://img.shields.io/badge/version-1.2.1-brightgreen?style=flat-square)
![version](https://img.shields.io/badge/version-1.2.2-brightgreen?style=flat-square)
![docker](https://img.shields.io/badge/docker-supported-blue?style=flat-square)

[![discord](https://img.shields.io/discord/667479986214666272?logo=discord&logoColor=white&style=flat-square)](https://diamonddigital.dev/discord)
Expand Down
25 changes: 19 additions & 6 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Allowed lifecycle actions: **start** from `stopped`/`crashed`; **stop** from `ru

## Servers

The server object returned by these endpoints contains the full configuration (name, type, version, port, memory, JVM args, gamemode, difficulty, seed, flags, `group`, timestamps, `state`, `exitCode`, `crashReason`, `javaMajor` — the Java runtime requirement recorded from Mojang metadata at jar download time, null when unknown, …). The on-disk `directory` field is stripped from responses.
The server object returned by these endpoints contains the full configuration (name, type, version, port, memory, JVM args, gamemode, difficulty, `levelType` (World Type, mirrored from `server.properties`), seed, flags, `group`, timestamps, `state`, `exitCode`, `crashReason`, `javaMajor` — the Java runtime requirement recorded from Mojang metadata at jar download time, null when unknown, …). The on-disk `directory` field is stripped from responses.

### Read

Expand All @@ -110,7 +110,7 @@ The server object returned by these endpoints contains the full configuration (n

| Method | Path | Description |
|---|---|---|
| POST | `/servers` | Create a server. Body: `{name, serverType, version, port, memory, eula, javaArgs?, gamemode?, difficulty?, seed?, group?, customJarUrl?}`. `eula` must be truthy; `customJarUrl` required (http/https) when `serverType` is `custom`. Returns `201 {"success": true, "server": {...}}`; provisioning continues in the background |
| POST | `/servers` | Create a server. Body: `{name, serverType, version, port, memory, eula, javaArgs?, gamemode?, difficulty?, levelType?, seed?, group?, customJarUrl?}`. `eula` must be truthy; `customJarUrl` required (http/https) when `serverType` is `custom`. `gamemode` defaults to `survival` and `difficulty` to `normal`. See [World Type](#world-type) for `levelType`. Returns `201 {"success": true, "server": {...}}`; provisioning continues in the background |
| POST | `/servers/from-modpack` | Create from a Modrinth modpack — see [Modrinth](#modrinth) |
| POST | `/servers/from-mrpack` | Create from an uploaded `.mrpack` file — see [Modrinth](#modrinth) |
| POST | `/servers/:id/duplicate` | Clone a server. Body: `{name, port, includeWorld?, stopFirst?, startAfter?}`. `409` if running and `stopFirst` is not set. Returns `201` |
Expand Down Expand Up @@ -145,7 +145,7 @@ The [WebSocket](#websocket-protocol) is the live feed, but it does not accept be

| Method | Path | Description |
|---|---|---|
| POST | `/servers/:id/edit` | Edit config. Body: `{name, port, memory, javaArgs?, gamemode?, difficulty?, seed?, group?, version?, customJarUrl?, backup?}`. Version changes must be upgrades (release versions compare numerically; snapshot/pre-release versions compare by the provider's chronological ordering) and require the server stopped (`409` otherwise); may download a new jar. Returns `{"success": true, "server": {...}, "versionChanged": bool, "jarChanged": bool}`. With `backup: true` see [Restore-point backups](#restore-point-backups) — returns `202` instead |
| POST | `/servers/:id/edit` | Edit config. Body: `{name, port, memory, javaArgs?, gamemode?, difficulty?, levelType?, seed?, group?, version?, customJarUrl?, backup?}`. Version changes must be upgrades (release versions compare numerically; snapshot/pre-release versions compare by the provider's chronological ordering) and require the server stopped (`409` otherwise); may download a new jar. Returns `{"success": true, "server": {...}, "versionChanged": bool, "jarChanged": bool}`. With `backup: true` see [Restore-point backups](#restore-point-backups) — returns `202` instead |
| POST | `/servers/:id/group` | Assign the dashboard group. Body: `{group}` (empty/null to ungroup). Returns `{"group": ..., "color": ...}` — `color` is the group's folder color (null when ungrouped) |
| POST | `/servers/:id/autorestart` | Body: `{enabled: bool}`. Returns `{"autoRestart": bool}` |
| POST | `/servers/:id/autostart` | Body: `{enabled: bool}`. Returns `{"autoStart": bool}` |
Expand All @@ -155,6 +155,19 @@ The [WebSocket](#websocket-protocol) is the live feed, but it does not accept be
| POST | `/servers/:id/properties` | Update `server.properties`. Body: an object keyed by property name, plus an optional `backup` flag (reserved — never written as a property). A partial update: properties left out keep their current value, and keys not already in the file are ignored. Toggles take `true`/`false` (boolean or string) — anything else is `400`. With `backup: true` see [Restore-point backups](#restore-point-backups) — returns `202` instead of `{"success": true}` |
| POST | `/servers/:id/edit-file` | Save a text file inside the server directory. Body: `{filePath, content}`. `403` on path traversal, `400` if the target is not text (see [Text vs binary](#files)) |

### World Type

`levelType` is `server.properties`' `level-type`, and like the seed it only applies when a new world is generated. Minecraft 1.19 renamed the world types and reads none of the old names it doesn't happen to share, so each version takes its own:

| Minecraft | Values |
|---|---|
| 1.19 and later (including 26.x) | `minecraft:normal`, `minecraft:flat`, `minecraft:large_biomes`, `minecraft:amplified`, `minecraft:single_biome_surface` |
| Before 1.19 | `default`, `flat`, `largeBiomes`, `amplified` |

Either spelling is accepted and stored in the one the server's version reads, so `flat` on a 26.x server is saved as `minecraft:flat`. A type the version has no equivalent of (Single Biome before 1.19) is `400`. Left out on create, it defaults to the version's normal world; left out on `/edit`, it keeps its value, except that a version change translates it into the new version's spelling. `/edit` also accepts the value already in the file as it stands, so a modpack's own world type survives a settings save.

New servers' `server.properties` carries every setting Minecraft only reads when it generates a world, so they can be set before the first start: `hardcore`, `level-type`, `generator-settings`, and from 1.19.3 `initial-enabled-packs` / `initial-disabled-packs`, each with the default that version would write itself.

### Files

Paths are relative to the server directory and are resolved against it with symlinks fully resolved — anything landing outside returns `403 {"error": "Access denied."}`.
Expand Down Expand Up @@ -383,10 +396,10 @@ Two quirks of Modrinth's search are worked around inside the proxy, so these end

| Method | Path | Description |
|---|---|---|
| POST | `/servers/from-modpack` | Body: `{projectId, versionId, name, port, memory, eula, javaArgs?, gamemode?, difficulty?, seed?, group?}`. Pack metadata is re-fetched server-side (client values cannot spoof it); the loader (Fabric/Forge/NeoForge) and Minecraft version come from the pack itself. Returns `201 {"success": true, "server": {...}}`; the install continues in the background (see progress below). `400` for Quilt packs, loaderless packs, or versions with no `.mrpack` file; `404`/`429`/`502` from the Modrinth lookups as above |
| POST | `/servers/from-modpack` | Body: `{projectId, versionId, name, port, memory, eula, javaArgs?, gamemode?, difficulty?, levelType?, seed?, group?}`. Pack metadata is re-fetched server-side (client values cannot spoof it); the loader (Fabric/Forge/NeoForge) and Minecraft version come from the pack itself. Returns `201 {"success": true, "server": {...}}`; the install continues in the background (see progress below). `400` for Quilt packs, loaderless packs, or versions with no `.mrpack` file; `404`/`429`/`502` from the Modrinth lookups as above |
| POST | `/servers/from-mrpack` | Create from an uploaded `.mrpack`. Multipart, file field `mrpack`, max 2 GiB, plus the same base fields as text fields. The pack is parsed and the loader resolved **before** any server record is created, so malformed or Quilt packs fail with a clean `400`. Returns `201` + background install. Also accepts [chunked uploads](#chunked-uploads-dgup) at `/servers/from-mrpack/upload/*` |

The background install downloads the pack's files (SHA-512 verified; download hosts restricted to the mrpack spec whitelist), installs the loader server pinned to the pack's loader version, and applies `overrides/` then `server-overrides/`. A `server.properties` the pack ships is kept: the port, gamemode and difficulty from the request are written over it (and the seed, when one is given — a blank seed keeps the pack's), `online-mode`, `enable-rcon`, `rcon.password` and `server-ip` are always reset to `true`, `false`, empty and empty, and Craftbox's defaults fill in any other key it leaves out. If the pack had set any of those four differently, the `complete` message's `warnings` says so. Mods the pack marks as unsupported on the server are still installed, but land disabled on disk and tagged `client` in the mod environment map — so they show as **Client Only** on the plugins page and are included in the status page's mods download for players, without the loader ever seeing them. Progress streams over the WebSocket as `operation: "modpack-install"`, `status: "progress"` messages with payload `{phase, done?, total?}` — phases: `download`, `parse`, `loader`, `files`, `overrides`, `finalize` — ending in `complete` or `failed`. The `files` phase carries `done`/`total` counts of **mods** (every jar destined for `mods/`, from the manifest and from the overrides, client-only ones included — so the total matches what the mods page lists afterwards, not the raw file count); it keeps ticking during the `overrides` phase as any mods shipped there land. On `failed` the half-built server is removed automatically (see [Asynchronous operations](#asynchronous-operations)). The created server records a `modpack` block (`{projectId, versionId, name, versionNumber, iconUrl, source: "modrinth"|"file", installedAt}`) for future tooling; it survives export/import.
The background install downloads the pack's files (SHA-512 verified; download hosts restricted to the mrpack spec whitelist), installs the loader server pinned to the pack's loader version, and applies `overrides/` then `server-overrides/`. No pack file, whether from the manifest or the overrides, may land among the server's launch files: anything under `libraries/` (where Forge and NeoForge keep the argument file handed to the JVM), a `.jar` or `*_args.txt` at the top of the server directory, `user_jvm_args.txt`, `run.sh`/`run.bat` or `fabric-server-launcher.properties`. Such files are skipped and named in the `complete` message's `warnings`, so a pack cannot change how the server is started. A `server.properties` the pack ships is kept: the port, gamemode and difficulty from the request are written over it (and the seed and `levelType`, when given — left blank, the pack's are kept), `online-mode`, `enable-rcon`, `rcon.password` and `server-ip` are always reset to `true`, `false`, empty and empty, and Craftbox's defaults fill in any other key it leaves out. If the pack had set any of those four differently, the `complete` message's `warnings` says so. Mods the pack marks as unsupported on the server are still installed, but land disabled on disk and tagged `client` in the mod environment map — so they show as **Client Only** on the plugins page and are included in the status page's mods download for players, without the loader ever seeing them. Progress streams over the WebSocket as `operation: "modpack-install"`, `status: "progress"` messages with payload `{phase, done?, total?}` — phases: `download`, `parse`, `loader`, `files`, `overrides`, `finalize` — ending in `complete` or `failed`. The `files` phase carries `done`/`total` counts of **mods** (every jar destined for `mods/`, from the manifest and from the overrides, client-only ones included — so the total matches what the mods page lists afterwards, not the raw file count); it keeps ticking during the `overrides` phase as any mods shipped there land. On `failed` the half-built server is removed automatically (see [Asynchronous operations](#asynchronous-operations)). The created server records a `modpack` block (`{projectId, versionId, name, versionNumber, iconUrl, source: "modrinth"|"file", installedAt}`) for future tooling; it survives export/import.

### Install a mod or plugin into an existing server

Expand All @@ -405,7 +418,7 @@ The background install downloads the pack's files (SHA-512 verified; download ho
| POST | `/templates` | Create from an existing server. Body: `{serverId, name, stopFirst?, startAfter?}`. Returns `201` |
| DELETE | `/templates/:id` | Delete a template |

Templates capture reusable configuration (type, version, memory, JVM args, gamemode, difficulty, port, behavior flags) — not world data, files, or dashboard groups.
Templates capture reusable configuration (type, version, memory, JVM args, gamemode, difficulty, world type, port, behavior flags) — not world data, files, or dashboard groups.


## API keys
Expand Down
Binary file modified docs/img/screenshots/create.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/screenshots/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "craftbox",
"version": "1.2.1",
"version": "1.2.2",
"description": "A modern self-hosted platform for managing Minecraft servers with built-in mod support.",
"main": "src/server.js",
"funding": {
Expand Down
62 changes: 62 additions & 0 deletions public/js/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,56 @@ const versionPicker = CraftboxVersionPicker({
function setVersionField(id) {
versionHidden.value = id || '';
versionDisplay.value = id || '';
if (!inModpackSource()) renderWorldTypes(selectedType === 'custom' ? '' : id);
}

// ── World Type ──
// Minecraft 1.19 renamed the world types (default → minecraft:normal) and
// reads no old name it doesn't happen to share, so the list follows the chosen
// version. A modpack keeps its own world type unless one is picked.
const levelTypeSelect = document.getElementById('levelType');
const WORLD_TYPES = JSON.parse(levelTypeSelect.dataset.worldTypes);
const MODPACK_WORLD_TYPE = { value: '', label: 'Modpack default' };

function inModpackSource() {
return createMode === 'modpack' || selectedSource === 'modpack';
}

// Mirrors isAtLeast(version, '1.19', '22w11a') in src/utils/mcVersion.js. A
// blank or unrecognised version (custom jars) counts as current.
function usesWorldPresets(version) {
const v = String(version || '').trim();
const snapshot = /^(\d{2})w(\d{2})/.exec(v);
if (snapshot) return Number(snapshot[1]) * 100 + Number(snapshot[2]) >= 2211;
const release = /^(\d+)\.(\d+)/.exec(v);
if (!release) return true;
return Number(release[1]) > 1 || Number(release[2]) >= 19;
}

// Rebuild the options for `version` (or the modpack list), keeping the
// current choice where the new list has the same world type under another
// name. `select` picks by label instead.
function renderWorldTypes(version, { modpack = false, select } = {}) {
const list = modpack
? [MODPACK_WORLD_TYPE].concat(WORLD_TYPES.presets)
: (usesWorldPresets(version) ? WORLD_TYPES.presets : WORLD_TYPES.legacy);
const wanted = select !== undefined ? select : (levelTypeSelect.selectedOptions[0] || {}).textContent;
levelTypeSelect.innerHTML = '';
list.forEach(function (t) {
const opt = document.createElement('option');
opt.value = t.value;
opt.textContent = t.label;
levelTypeSelect.appendChild(opt);
});
const keep = list.find(function (t) { return t.label === wanted; });
levelTypeSelect.value = keep ? keep.value : list[0].value;
}

// A saved world type's label, whichever list it came from
function worldTypeLabel(value) {
const all = WORLD_TYPES.presets.concat(WORLD_TYPES.legacy);
const match = all.find(function (t) { return t.value.toLowerCase() === String(value || '').toLowerCase(); });
return match ? match.label : undefined;
}

function openVersionPicker() {
Expand Down Expand Up @@ -194,6 +244,7 @@ async function selectType(typeId) {
customUrlGroup.classList.remove('d-none');
versionDisplay.removeAttribute('required');
setCustomNoticeVisible(true);
renderWorldTypes('');
centerLoneRowItems(form);
} else {
versionGroup.classList.remove('d-none');
Expand Down Expand Up @@ -340,6 +391,9 @@ templateSelect.addEventListener('change', async () => {
if (t.port) document.getElementById('port').value = t.port;
if (t.gamemode) document.getElementById('gamemode').value = t.gamemode;
if (t.difficulty) document.getElementById('difficulty').value = t.difficulty;
if (t.levelType && worldTypeLabel(t.levelType)) {
renderWorldTypes(t.serverType === 'custom' ? '' : versionHidden.value, { select: worldTypeLabel(t.levelType) });
}

// Advanced options
if (t.memory && t.memory !== 2048) {
Expand Down Expand Up @@ -380,6 +434,7 @@ function collectBaseFields() {
javaArgs: document.getElementById('javaArgs').value,
gamemode: document.getElementById('gamemode').value,
difficulty: document.getElementById('difficulty').value,
levelType: levelTypeSelect.value,
seed: document.getElementById('seed').value,
group: document.getElementById('group').value
};
Expand Down Expand Up @@ -438,6 +493,7 @@ async function enterModpackMode() {
createSourceGroup.classList.add('d-none');
hideTypeAndVersionPickers();
modpackSummary.classList.remove('d-none');
renderWorldTypes(null, { modpack: true, select: MODPACK_WORLD_TYPE.label });
suggestModpackMemory();
validateCreateForm();

Expand Down Expand Up @@ -526,6 +582,7 @@ function applySource(source) {
versionDisplay.removeAttribute('required');
customUrlGroup.classList.add('d-none');
setCustomNoticeVisible(false);
renderWorldTypes(null, { modpack: true, select: MODPACK_WORLD_TYPE.label });
suggestModpackMemory();
} else {
mrpackFileInput.removeAttribute('required');
Expand All @@ -537,6 +594,11 @@ function applySource(source) {
versionGroup.classList.remove('d-none');
versionDisplay.setAttribute('required', '');
}
// Back from the modpack list: "Modpack default" has no counterpart,
// so this falls to the version's default unless a type was picked
if (levelTypeSelect.querySelector('option[value=""]')) {
renderWorldTypes(selectedType === 'custom' ? '' : versionHidden.value);
}
revertModpackMemory();
}
validateCreateForm();
Expand Down
Loading
Loading