diff --git a/.gitignore b/.gitignore
index 95826f89..64527bda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
/staging/
/packs/behavior/pack_icon.png
/packs/resource/pack_icon.png
+/out
# misc
.DS_Store
diff --git a/eslint.config.js b/eslint.config.js
index 818596ad..361d6cdc 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -6,7 +6,6 @@ import importPlugin from 'eslint-plugin-import';
import jsonc from 'eslint-plugin-jsonc';
import minecraftLinting from 'eslint-plugin-minecraft-linting';
import promisePlugin from 'eslint-plugin-promise';
-import sonarjs from 'eslint-plugin-sonarjs';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import unusedImports from 'eslint-plugin-unused-imports';
import globals from 'globals';
@@ -37,20 +36,6 @@ export default tseslint.config(
// Base JS configuration
eslint.configs.recommended,
- // SonarJS Configuration
- sonarjs.configs.recommended,
- {
- rules: {
- // Disable rules that conflict with strict TS or are too noisy for this project
- 'sonarjs/no-duplicate-string': 'off', // Common in Minecraft commands/IDs
- 'sonarjs/cognitive-complexity': ['warn', 60], // Increased limit for complex UI handlers
- 'sonarjs/no-nested-template-literals': 'off',
- 'sonarjs/todo-tag': 'warn',
- 'sonarjs/fixme-tag': 'warn',
- 'sonarjs/pseudo-random': 'off' // Safe for Minecraft game mechanics
- }
- },
-
// Unicorn Configuration
eslintPluginUnicorn.configs['flat/recommended'],
{
@@ -230,9 +215,7 @@ export default tseslint.config(
...jsonc.configs['flat/recommended-with-jsonc'],
{
files: ['**/*.json'],
- rules: {
- 'sonarjs/no-empty-test-file': 'off'
- }
+ rules: {}
},
// Prettier
diff --git a/package.json b/package.json
index e6660adb..31a82355 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,6 @@
"description": "
AddonExe for Minecraft BE
",
"main": "index.js",
"scripts": {
- "preinstall": "node scripts/update-mc-deps.js",
"clean": "rimraf packs/behavior/scripts",
"reinstall": "rm -rf node_modules package-lock.json && npm install",
"check-types": "tsc --noEmit",
@@ -47,11 +46,11 @@
"@minecraft/common": "latest",
"@minecraft/core-build-tasks": "latest",
"@minecraft/creator-tools": "latest",
- "@minecraft/debug-utilities": "1.0.0-beta.1.26.1-stable",
+ "@minecraft/debug-utilities": "beta",
"@minecraft/math": "latest",
- "@minecraft/server": "2.6.0-beta.1.26.1-stable",
- "@minecraft/server-gametest": "1.0.0-beta.1.26.1-stable",
- "@minecraft/server-ui": "2.1.0-beta.1.26.1-stable",
+ "@minecraft/server": "beta",
+ "@minecraft/server-gametest": "beta",
+ "@minecraft/server-ui": "beta",
"@minecraft/vanilla-data": "latest",
"@types/jest": "latest",
"@types/node": "latest",
@@ -68,7 +67,6 @@
"eslint-plugin-jsonc": "latest",
"eslint-plugin-minecraft-linting": "latest",
"eslint-plugin-promise": "latest",
- "eslint-plugin-sonarjs": "^3.0.6",
"eslint-plugin-unicorn": "latest",
"eslint-plugin-unused-imports": "latest",
"glob": "latest",
@@ -78,7 +76,7 @@
"prettier": "latest",
"prettier-plugin-organize-imports": "latest",
"rimraf": "latest",
- "simple-git-hooks": "^2.13.1",
+ "simple-git-hooks": "latest",
"ts-jest": "latest",
"ts-node": "latest",
"tsc-alias": "latest",
@@ -129,10 +127,10 @@
},
"overrides": {
"@minecraft/common": "latest",
- "@minecraft/debug-utilities": "1.0.0-beta.1.26.1-stable",
- "@minecraft/server": "2.6.0-beta.1.26.1-stable",
- "@minecraft/server-gametest": "1.0.0-beta.1.26.1-stable",
- "@minecraft/server-ui": "2.1.0-beta.1.26.1-stable",
+ "@minecraft/debug-utilities": "beta",
+ "@minecraft/server": "beta",
+ "@minecraft/server-gametest": "beta",
+ "@minecraft/server-ui": "beta",
"@minecraft/vanilla-data": "latest"
}
}
diff --git a/packs/resource/ui/_ui_defs.json b/packs/resource/ui/_ui_defs.json
new file mode 100644
index 00000000..ce4d631e
--- /dev/null
+++ b/packs/resource/ui/_ui_defs.json
@@ -0,0 +1,10 @@
+{
+ "ui_defs": [
+ "ui/hud_screen.json",
+ "ui/loading_messages.json",
+ "ui/pause_screen.json",
+ "ui/scoreboards.json",
+ "ui/server_form.json",
+ "ui/tic_tac_toe.json"
+ ]
+}
diff --git a/packs/resource/ui/pause_screen.json b/packs/resource/ui/pause_screen.json
index b21075e3..4e6681fa 100644
--- a/packs/resource/ui/pause_screen.json
+++ b/packs/resource/ui/pause_screen.json
@@ -16,27 +16,5 @@
}
}
]
- },
- "store_button_panel@pause.pause_button_template": {
- "$pressed_button_name": "button.menu_settings",
- "$button_text": "menu.settings"
- },
- "marketplace_icon": {
- "type": "panel",
- "size": [16, 16],
- "controls": [
- {
- "icon": {
- "type": "image",
- "texture": "textures/ui/sidebar_icons/marketplace",
- "size": [16, 16]
- }
- }
- ]
- },
- "settings_button_small@common_buttons.light_content_button": {
- "$pressed_button_name": "button.menu_store",
- "$button_tts_header": "menu.store",
- "$button_content": "pause.marketplace_icon"
}
}
diff --git a/packs/resource/ui/server_form.json b/packs/resource/ui/server_form.json
index 27aaa2cc..e53c7cf8 100644
--- a/packs/resource/ui/server_form.json
+++ b/packs/resource/ui/server_form.json
@@ -2,6 +2,11 @@
"namespace": "server_form",
"long_form": {
"type": "panel",
+ "bindings": [
+ { "binding_name": "#title_text" },
+ { "binding_name": "#content_text" },
+ { "binding_name": "#form_button_length" }
+ ],
"controls": [
{
"client_content": {
@@ -19,8 +24,7 @@
"controls": [
{
"grid_view": {
- "type": "custom",
- "renderer": "ui_scene_renderer",
+ "type": "panel",
"layer": 1,
"anchor_from": "center",
"anchor_to": "center",
@@ -29,7 +33,7 @@
}
}
],
- "visible": "(($title_text - '§t§t§t') = 'Tic Tac Toe')"
+ "visible": "($title_text = '§t§t§tTic Tac Toe')"
}
},
{
@@ -99,7 +103,7 @@
}
}
],
- "visible": "(not (($title_text - '§t§t§t') = 'Tic Tac Toe'))"
+ "visible": "(not ($title_text = '§t§t§tTic Tac Toe'))"
}
}
]
diff --git a/scripts/generate-manifests.js b/scripts/generate-manifests.js
index 69dd4aa5..0927a8b1 100644
--- a/scripts/generate-manifests.js
+++ b/scripts/generate-manifests.js
@@ -1,7 +1,10 @@
+import { exec } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
+import { promisify } from 'node:util';
+const execAsync = promisify(exec);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -51,23 +54,36 @@ function parseVersion(versionString) {
}
/**
- * Extracts the module version from the NPM version string.
- * e.g., "2.5.0-beta.1.21.132-stable" -> "2.5.0-beta"
- * "1.0.0" -> "1.0.0"
+ * Fetches the latest stable version of a package from npm.
+ * @param {string} pkgName The name of the package.
+ * @returns {Promise} The latest version.
*/
-function extractModuleVersion(npmVersion) {
+async function fetchLatestVersion(pkgName) {
+ try {
+ console.log(`Fetching latest version for ${pkgName}...`);
+ const { stdout } = await execAsync(`npm view ${pkgName} version`);
+ return stdout.trim();
+ } catch (error) {
+ console.warn(`Failed to fetch version for ${pkgName}, defaulting to 1.0.0. Error: ${error.message}`);
+ return '1.0.0';
+ }
+}
+
+/**
+ * Resolves the module version from the NPM version string.
+ */
+async function resolveModuleVersion(pkgName, npmVersion) {
if (!npmVersion) return '1.0.0';
- if (npmVersion === 'latest') return 'beta';
- // Strip range characters like ^, ~, >=
- const cleanVersion = npmVersion.replace(/^[^\d]*/, '');
+ if (npmVersion === 'beta') return 'beta';
- // Regex to capture x.y.z(-tag)?
- const match = cleanVersion.match(/^(\d+\.\d+\.\d+(?:-(?:beta|rc|preview))?)/);
- if (match) {
- return match[1];
+ if (npmVersion === 'latest') {
+ const version = await fetchLatestVersion(pkgName);
+ return version;
}
- return cleanVersion;
+
+ // Strip range characters like ^, ~, >= if explicit version provided
+ return npmVersion.replace(/^[^\d]*/, '');
}
async function generateManifests() {
@@ -116,14 +132,19 @@ async function generateManifests() {
const dependencies = [];
- for (const mod of modulesToInclude) {
- if (devDeps[mod]) {
- dependencies.push({
+ // Process modules in parallel to speed up npm fetches
+ const modulePromises = modulesToInclude
+ .filter((mod) => devDeps[mod])
+ .map(async (mod) => {
+ const version = await resolveModuleVersion(mod, devDeps[mod]);
+ return {
module_name: mod,
- version: extractModuleVersion(devDeps[mod])
- });
- }
- }
+ version: version
+ };
+ });
+
+ const resolvedModules = await Promise.all(modulePromises);
+ dependencies.push(...resolvedModules);
// Always add RP dependency
dependencies.push({
diff --git a/scripts/update-mc-deps.js b/scripts/update-mc-deps.js
deleted file mode 100644
index 0dea485b..00000000
--- a/scripts/update-mc-deps.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import { exec } from 'node:child_process';
-import { constants } from 'node:fs';
-import fs from 'node:fs/promises';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { promisify } from 'node:util';
-
-const execAsync = promisify(exec);
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const packageJsonPath = path.resolve(__dirname, '../package.json');
-
-// Color codes for console output
-const colors = {
- reset: '\u001B[0m',
- green: '\u001B[32m',
- yellow: '\u001B[33m',
- blue: '\u001B[34m',
- red: '\u001B[31m'
-};
-
-async function fetchPackageVersion(pkg) {
- try {
- const { stdout } = await execAsync(`npm view ${pkg} versions --json`);
- const versions = JSON.parse(stdout);
-
- // Filter: Must include 'beta', end with '-stable', and NOT include 'preview'
- const candidates = versions
- .filter((v) => v.includes('beta') && v.endsWith('-stable') && !v.includes('preview'))
- .toSorted((a, b) => a.localeCompare(b, undefined, { numeric: true }));
-
- if (candidates.length === 0) {
- return null;
- }
- // Pick the last one (which is the latest due to numeric sort)
- return candidates.at(-1);
- } catch (error) {
- console.error(`${colors.yellow}Warning: Failed to fetch versions for ${pkg}: ${error.message}${colors.reset}`);
- return null;
- }
-}
-
-async function updatePackageJson(updates, dryRun = false) {
- try {
- await fs.access(packageJsonPath, constants.F_OK);
- } catch {
- console.error(`${colors.red}Error: package.json not found at ${packageJsonPath}${colors.reset}`);
- return false;
- }
-
- const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
- const packageJson = JSON.parse(packageJsonContent);
-
- const devDeps = packageJson.devDependencies || {};
- const overrides = packageJson.overrides || {};
-
- // Identify targets: @minecraft/* packages in devDependencies that are NOT 'latest'
- const targets = Object.keys(devDeps).filter((pkg) => pkg.startsWith('@minecraft/') && devDeps[pkg] !== 'latest');
-
- if (targets.length === 0) {
- return false;
- }
-
- console.log(`${colors.blue}Checking for updates for ${targets.length} packages...${colors.reset}`);
-
- const promiseResults = await Promise.allSettled(
- targets.map(async (pkg) => {
- const newVersion = await fetchPackageVersion(pkg);
- return { pkg, newVersion };
- })
- );
-
- let packageJsonUpdated = false;
- for (const result of promiseResults) {
- if (result.status === 'fulfilled' && result.value.newVersion) {
- const { pkg, newVersion } = result.value;
- const currentVersion = devDeps[pkg];
-
- if (currentVersion !== newVersion) {
- // Only update the object in memory if we are going to write it or if we need to report it.
- // If dryRun is true, we still want to report what WOULD change.
-
- // We update the local object so we can print the report, but we won't write if dryRun is true.
- // Actually, let's keep the logic simple: update the object, just don't write to file.
- devDeps[pkg] = newVersion;
-
- // Update overrides if the package exists there
- if (overrides[pkg]) {
- overrides[pkg] = newVersion;
- }
-
- updates.push(`[package.json] ${pkg}: ${currentVersion} -> ${newVersion}`);
- packageJsonUpdated = true;
- }
- }
- }
-
- if (packageJsonUpdated) {
- if (!dryRun) {
- const indentation = 4;
- await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, undefined, indentation) + '\n');
- }
- return true;
- }
-
- return false;
-}
-
-async function main() {
- const isCI = !!process.env.CI;
- console.log(`${colors.blue}Checking Minecraft dependencies...${isCI ? '(CI Mode)' : ''}${colors.reset}`);
-
- const updates = [];
- const pkgUpdated = await updatePackageJson(updates, isCI);
-
- if (pkgUpdated) {
- if (isCI) {
- console.warn(`${colors.yellow}Updates available for Minecraft dependencies:${colors.reset}`);
- for (const u of updates) console.warn(` ${u}`);
- console.warn(
- `${colors.blue}Skipping auto-update in CI environment. Run 'npm install' locally to update.${colors.reset}`
- );
- // Exit with 0 so CI doesn't fail
- process.exit(0);
- } else {
- console.log(`${colors.green}Updated Minecraft dependencies:${colors.reset}`);
- for (const u of updates) console.log(` ${u}`);
- console.log(
- `${colors.yellow}Dependencies updated. Please run 'npm install' again to install the new versions.${colors.reset}`
- );
- // Exit with 1 to indicate changes were made (useful for local hooks)
- process.exit(1);
- }
- } else {
- console.log(`${colors.green}All Minecraft dependencies are up to date.${colors.reset}`);
- process.exit(0);
- }
-}
-
-main().catch((error) => {
- console.error(`${colors.red}Fatal error updating dependencies: ${error}${colors.reset}`);
- process.exit(1);
-});
diff --git a/src/core/__tests__/UI.test.ts b/src/core/__tests__/UI.test.ts
index 35d0bd0d..acaaf0fa 100644
--- a/src/core/__tests__/UI.test.ts
+++ b/src/core/__tests__/UI.test.ts
@@ -15,7 +15,8 @@ jest.unstable_mockModule('../configManager.js', () => ({
updateMultipleConfig: jest.fn(),
resetConfigSection: jest.fn(),
onConfigUpdated: jest.fn(),
- initializeConfigManager: jest.fn()
+ initializeConfigManager: jest.fn(),
+ reloadConfig: jest.fn()
}));
// Mock feature managers if needed by panels
diff --git a/src/core/__tests__/__mocks__/minecraftMock.ts b/src/core/__tests__/__mocks__/minecraftMock.ts
index 83d25b34..775620cc 100644
--- a/src/core/__tests__/__mocks__/minecraftMock.ts
+++ b/src/core/__tests__/__mocks__/minecraftMock.ts
@@ -170,7 +170,6 @@ export class ModalFormData {
show = jest.fn().mockImplementation(async () => {
return {
- // eslint-disable-next-line sonarjs/function-return-type
formValues: this._controls.map((c): string | number | boolean | undefined => {
if (c.type === 'toggle') return c.defaultValue ?? false;
if (c.type === 'textField') return c.defaultValue ?? '';
diff --git a/src/core/events/beforeChatSend.ts b/src/core/events/beforeChatSend.ts
index 560d2dcf..4c5ca9b4 100644
--- a/src/core/events/beforeChatSend.ts
+++ b/src/core/events/beforeChatSend.ts
@@ -1,5 +1,6 @@
import * as mc from '@minecraft/server';
+import { commandManager } from '@core/commands/commandManager.js';
import { getConfig } from '@core/configManager.js';
import { getPlayerFromCache } from '@core/playerCache.js';
import { getPlayer } from '@core/playerDataManager.js';
@@ -9,6 +10,8 @@ import { getPlayer } from '@core/playerDataManager.js';
* Manages chat formatting, muting, and ranks.
*/
export default function handleBeforeChatSend(event: mc.ChatSendBeforeEvent) {
+ if (commandManager.handleChatCommand(event)) return;
+
const config = getConfig();
if (config.chat.enabled !== true) return; // Vanilla chat if disabled
diff --git a/src/core/main.ts b/src/core/main.ts
index ce8fdf38..19f4816e 100644
--- a/src/core/main.ts
+++ b/src/core/main.ts
@@ -1,5 +1,6 @@
import * as mc from '@minecraft/server';
+import { loadCommands } from '@core/commands/index.js';
import { initializeXrayDetection } from '@features/anticheat/xrayDetection.js';
import { restartAnnouncer } from '@features/essentials/commands/announcement.js';
import { cleanupSpawnProtection, initializeSpawnProtection } from '@features/essentials/spawnProtection.js';
@@ -33,6 +34,9 @@ import { reinitializeOnlinePlayers } from './utils.js';
const VERSION = '0.7.0'; // Current Addon Version
+// Load Commands immediately to register slash commands during startup
+loadCommands();
+
/**
* Initializes the addon.
* This function should be called once at startup.
diff --git a/src/core/playerDataManager.ts b/src/core/playerDataManager.ts
index 11482aba..cbf001f2 100644
--- a/src/core/playerDataManager.ts
+++ b/src/core/playerDataManager.ts
@@ -173,7 +173,6 @@ function saveShardedMap(map: Map, prefix: string) {
// Cleanup stale shards if the map shrank
let nextIndex = totalShards;
while (mc.world.getDynamicProperty(`${prefix}${nextIndex}`) !== undefined) {
- // eslint-disable-next-line sonarjs/no-undefined-argument
mc.world.setDynamicProperty(`${prefix}${nextIndex}`, undefined);
nextIndex++;
}
@@ -193,7 +192,6 @@ function loadShardedMap(map: Map, legacyKey: string, shardPrefix
const entries = JSON.parse(legacyData) as [string, string][];
for (const [k, v] of entries) map.set(k, v);
// Delete legacy key immediately to mark migration complete
- // eslint-disable-next-line sonarjs/no-undefined-argument
mc.world.setDynamicProperty(legacyKey, undefined);
migrated = true;
} catch (error) {
diff --git a/src/core/storage/StorageManager.ts b/src/core/storage/StorageManager.ts
index 7d64bd3f..a853f6a7 100644
--- a/src/core/storage/StorageManager.ts
+++ b/src/core/storage/StorageManager.ts
@@ -31,7 +31,6 @@ export class StorageManager {
// Cleanup old chunks if size shrank
let nextIndex = chunks;
while (mc.world.getDynamicProperty(`${this.dbName}:${nextIndex}`) !== undefined) {
- // eslint-disable-next-line sonarjs/no-undefined-argument
mc.world.setDynamicProperty(`${this.dbName}:${nextIndex}`, undefined); // Delete
nextIndex++;
}
@@ -64,7 +63,6 @@ export class StorageManager {
// Cleanup old chunks
let nextIndex = chunks;
while (mc.world.getDynamicProperty(`${this.dbName}:${nextIndex}`) !== undefined) {
- // eslint-disable-next-line sonarjs/no-undefined-argument
mc.world.setDynamicProperty(`${this.dbName}:${nextIndex}`, undefined); // Delete
nextIndex++;
if (nextIndex % 5 === 0) yield;
@@ -118,13 +116,10 @@ export class StorageManager {
const chunks = typeof meta === 'number' ? meta : 0;
for (let i = 0; i < chunks; i++) {
- // eslint-disable-next-line sonarjs/no-undefined-argument
mc.world.setDynamicProperty(`${this.dbName}:${i}`, undefined);
}
- // eslint-disable-next-line sonarjs/no-undefined-argument
mc.world.setDynamicProperty(`${this.dbName}:meta`, undefined);
// Try legacy clean up too
- // eslint-disable-next-line sonarjs/no-undefined-argument
mc.world.setDynamicProperty(this.dbName, undefined);
} catch (error) {
errorLog(`[StorageManager] Failed to delete ${this.dbName}`, error);
diff --git a/src/core/utils/sanitization.ts b/src/core/utils/sanitization.ts
index 455f4a1f..71de9dfd 100644
--- a/src/core/utils/sanitization.ts
+++ b/src/core/utils/sanitization.ts
@@ -14,7 +14,7 @@ export function sanitizeString(input: string, allowColors = false): string {
}
// Remove non-printable characters (basic control chars, keeping newlines/returns)
- // eslint-disable-next-line no-control-regex, sonarjs/no-control-regex
+ // eslint-disable-next-line no-control-regex
result = result.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F]/g, '');
return result.trim();