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();