Upload via GUI (90 Dateien)

This commit is contained in:
2026-10-02 06:22:55 +00:00
parent 906ac75443
commit 47b8b88f6e
14 changed files with 12117 additions and 11013 deletions
+85 -70
View File
@@ -1,70 +1,85 @@
'use strict';
/*
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
* -----------------------------------------------------------------------------
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
* (siehe wordpress-plugin/aeromc-friends/). Keine eigene Anmeldung - jeder
* Aufruf trägt einfach die Minecraft-UUID des aktiven Kontos.
*
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
* nie den Spielstart oder die Bedienung blockieren.
*/
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
async function anfrage(pfad, { method = 'GET', body } = {}) {
try {
const url = method === 'GET' && body
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
: `${BASIS_URL}${pfad}`;
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
});
const daten = await res.json().catch(() => null);
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
return { ok: true, data: daten };
} catch (err) {
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
}
}
function heartbeat({ uuid, username, instance, server }) {
return anfrage('/presence', { method: 'POST', body: { uuid, username, instance, server } });
}
function getFriends(uuid) {
return anfrage('/friends', { method: 'GET', body: { uuid } });
}
function getRequests(uuid) {
return anfrage('/friends/requests', { method: 'GET', body: { uuid } });
}
function getSentRequests(uuid) {
return anfrage('/friends/sent', { method: 'GET', body: { uuid } });
}
function sendRequest(uuid, username, targetUsername) {
return anfrage('/friends/request', { method: 'POST', body: { uuid, username, targetUsername } });
}
function respond(uuid, otherUuid, accept) {
return anfrage('/friends/respond', { method: 'POST', body: { uuid, otherUuid, accept } });
}
function cancelRequest(uuid, otherUuid) {
return anfrage('/friends/cancel', { method: 'POST', body: { uuid, otherUuid } });
}
function removeFriend(uuid, otherUuid) {
return anfrage('/friends/remove', { method: 'POST', body: { uuid, otherUuid } });
}
module.exports = {
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
};
'use strict';
/*
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
* -----------------------------------------------------------------------------
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
* (siehe wordpress-plugin/aeromc-friends/). Keine eigene Anmeldung - jeder
* Aufruf trägt einfach die Minecraft-UUID des aktiven Kontos.
*
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
* message } gereicht statt den Launcher zu stören - ein soziales Extra darf
* nie den Spielstart oder die Bedienung blockieren.
*/
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
async function anfrage(pfad, { method = 'GET', body } = {}) {
try {
const url = method === 'GET' && body
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
: `${BASIS_URL}${pfad}`;
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
});
const daten = await res.json().catch(() => null);
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
return { ok: true, data: daten };
} catch (err) {
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
}
}
function heartbeat({ uuid, username, instance, server, version }) {
return anfrage('/presence', { method: 'POST', body: { uuid, username, instance, server, version } });
}
function getFriends(uuid) {
return anfrage('/friends', { method: 'GET', body: { uuid } });
}
function getRequests(uuid) {
return anfrage('/friends/requests', { method: 'GET', body: { uuid } });
}
function getSentRequests(uuid) {
return anfrage('/friends/sent', { method: 'GET', body: { uuid } });
}
function sendRequest(uuid, username, targetUsername) {
return anfrage('/friends/request', { method: 'POST', body: { uuid, username, targetUsername } });
}
function respond(uuid, otherUuid, accept) {
return anfrage('/friends/respond', { method: 'POST', body: { uuid, otherUuid, accept } });
}
function cancelRequest(uuid, otherUuid) {
return anfrage('/friends/cancel', { method: 'POST', body: { uuid, otherUuid } });
}
function removeFriend(uuid, otherUuid) {
return anfrage('/friends/remove', { method: 'POST', body: { uuid, otherUuid } });
}
// Einladung "Komm auf meinen Server" (Adresse + Minecraft-Version des Einladenden)
function sendInvite(uuid, targetUuid, server, version) {
return anfrage('/friends/invite', { method: 'POST', body: { uuid, targetUuid, server, version } });
}
// an mich gerichtete, noch gültige Einladungen
function getInvites(uuid) {
return anfrage('/friends/invites', { method: 'GET', body: { uuid } });
}
function dismissInvite(uuid, id) {
return anfrage('/friends/invites/dismiss', { method: 'POST', body: { uuid, id } });
}
module.exports = {
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
sendInvite, getInvites, dismissInvite,
};
+1531 -1473
View File
File diff suppressed because it is too large Load Diff
+17 -2
View File
@@ -357,6 +357,13 @@ function buildArgs(vj, ctx) {
* auth: null (kein Login) ODER { name, uuid, accessToken, userType }
* onProgress: (evt) => void
*/
// true für Release-Versionen vor 1.20 (kein Quick Play). Snapshots und
// Unbekanntes gelten als neu – die meisten heutigen Snapshots sind > 1.20.
function vorQuickPlay(versionId) {
const m = /^1\.(\d+)(?:\.\d+)?(?:-(?:pre|rc)\d+)?$/.exec(String(versionId || '').trim());
return !!m && Number(m[1]) < 20;
}
async function prepareAndLaunch(opts, onProgress) {
const emit = (phase, extra) => onProgress && onProgress(Object.assign({ phase }, extra));
@@ -526,9 +533,17 @@ async function prepareAndLaunch(opts, onProgress) {
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
if (opts.mcMaximized) args.push('--fullscreen');
else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight));
// Ab 1.20: direkt einem Server beitreten (Quick Play)
// Direkt einem Server beitreten: ab 1.20 per Quick Play, davor über die
// älteren Startparameter --server/--port (z. B. für "Beitreten" bei Freunden)
if (opts.quickPlayServer) {
args.push('--quickPlayMultiplayer', String(opts.quickPlayServer));
const adresse = String(opts.quickPlayServer).trim();
if (vorQuickPlay(opts.versionId)) {
const m = /^(.*?)(?::(\d+))?$/.exec(adresse);
args.push('--server', m[1]);
if (m[2]) args.push('--port', m[2]);
} else {
args.push('--quickPlayMultiplayer', adresse);
}
}
ensureDir(opts.gameDir);
+3351 -3015
View File
File diff suppressed because it is too large Load Diff
+352 -327
View File
@@ -1,327 +1,352 @@
'use strict';
/*
* preload.js – stellt dem Renderer eine schmale, sichere API bereit.
* contextIsolation ist aktiv; der Renderer hat KEINEN direkten Node-Zugriff.
*/
const { contextBridge, ipcRenderer } = require('electron');
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
// damit man sie auch ohne offene DevTools sieht
window.addEventListener('error', (e) => {
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
});
window.addEventListener('unhandledrejection', (e) => {
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
});
contextBridge.exposeInMainWorld('api', {
// Einstellungen
getSettings: () => ipcRenderer.invoke('settings:get'),
saveSettings: (patch) => ipcRenderer.invoke('settings:save', patch),
getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'),
detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath),
scanJava: () => ipcRenderer.invoke('java:scan'),
listJava: () => ipcRenderer.invoke('java:list'),
// Minecraft-Versionen (Mojang)
mcVersions: () => ipcRenderer.invoke('mc:versions'),
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
// Mods (Modrinth)
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
curseforgeTestKey: (apiKey) => ipcRenderer.invoke('curseforge:testKey', apiKey),
// art: 'mod' | 'resourcepack' | 'shader'
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
systemMemory: () => ipcRenderer.invoke('system:memory'),
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
onAutoBackup: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('backup:auto', l);
return () => ipcRenderer.removeListener('backup:auto', l);
},
crashOpenFolder: (id) => ipcRenderer.invoke('crash:openFolder', id),
serversList: (id) => ipcRenderer.invoke('servers:list', id),
serversAdd: (id, eintrag) => ipcRenderer.invoke('servers:add', id, eintrag),
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
logsList: (id) => ipcRenderer.invoke('logs:list', id),
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
modCheckUpdates: (id, art) => ipcRenderer.invoke('mods:checkUpdates', id, art),
modApplyUpdates: (id, projectIds, art) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds, art),
onModUpdateProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('mods:updateProgress', l);
return () => ipcRenderer.removeListener('mods:updateProgress', l);
},
// Instanzen
listInstances: () => ipcRenderer.invoke('instances:list'),
getInstance: (id) => ipcRenderer.invoke('instances:get', id),
createInstance: (data) => ipcRenderer.invoke('instances:create', data),
updateInstance: (id, patch) => ipcRenderer.invoke('instances:update', id, patch),
deleteInstance: (id) => ipcRenderer.invoke('instances:delete', id),
duplicateInstance: (id) => ipcRenderer.invoke('instances:duplicate', id),
openInstanceFolder: (id) => ipcRenderer.invoke('instances:openFolder', id),
openGameDir: (id) => ipcRenderer.invoke('instances:openGameDir', id),
// Welten & Ressourcenpakete
worlds: (id) => ipcRenderer.invoke('instance:worlds', id),
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
openScreenshot: (id, name) => ipcRenderer.invoke('instance:openScreenshot', id, name),
launchInstance: (id, accountId, opts) => ipcRenderer.invoke('instances:launch', id, accountId, opts),
exportInstance: (id) => ipcRenderer.invoke('instances:export', id),
createGlobalBackup: () => ipcRenderer.invoke('backup:global'),
listGlobalBackups: () => ipcRenderer.invoke('backup:list'),
restoreGlobalBackup: (sourcePath) => ipcRenderer.invoke('backup:restore', sourcePath),
onBackupProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('backup:progress', l);
return () => ipcRenderer.removeListener('backup:progress', l);
},
importInstance: () => ipcRenderer.invoke('instances:import'),
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
onLaunchRequest: (cb) => {
const l = (_e, payload) => cb(payload);
ipcRenderer.on('instance:launch-request', l);
return () => ipcRenderer.removeListener('instance:launch-request', l);
},
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
notifyReady: () => ipcRenderer.send('renderer:ready'),
importModpack: () => ipcRenderer.invoke('modpack:import'),
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
onLaunchProgress: (cb) => {
const listener = (_e, data) => cb(data);
ipcRenderer.on('launch:progress', listener);
return () => ipcRenderer.removeListener('launch:progress', listener);
},
// eigenes Instanz-Bild
pickImage: () => ipcRenderer.invoke('dialog:pickImage'),
setInstanceImage: (id, sourcePath) => ipcRenderer.invoke('instances:setImage', id, sourcePath),
clearInstanceImage: (id) => ipcRenderer.invoke('instances:clearImage', id),
instanceIcon: (id) => ipcRenderer.invoke('instances:icon', id),
fileDataUri: (p) => ipcRenderer.invoke('file:dataUri', p),
// Microsoft-Login (Phase 3) + Mehrkonten
authStatus: () => ipcRenderer.invoke('auth:status'),
authAccounts: () => ipcRenderer.invoke('auth:accounts'),
authSetActive: (id) => ipcRenderer.invoke('auth:setActive', id),
skinProfile: () => ipcRenderer.invoke('skin:profile'),
skinChoose: () => ipcRenderer.invoke('skin:choose'),
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
skinReset: () => ipcRenderer.invoke('skin:reset'),
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
authLogin: () => ipcRenderer.invoke('auth:login'),
authLogout: () => ipcRenderer.invoke('auth:logout'),
authHealth: () => ipcRenderer.invoke('auth:health'),
authRecheck: () => ipcRenderer.invoke('auth:recheck'),
onAuthHealth: (cb) => {
const listener = (_e, data) => cb(data);
ipcRenderer.on('auth:health', listener);
return () => ipcRenderer.removeListener('auth:health', listener);
},
// meldet sich, wenn ein Spiel startet oder endet -> Kontenliste auffrischen
onAccountsChanged: (cb) => {
const listener = () => cb();
ipcRenderer.on('accounts:changed', listener);
return () => ipcRenderer.removeListener('accounts:changed', listener);
},
onAuthRefreshed: (cb) => {
const listener = () => cb();
ipcRenderer.on('auth:refreshed', listener);
return () => ipcRenderer.removeListener('auth:refreshed', listener);
},
onAuthCode: (cb) => {
const listener = (_e, data) => cb(data);
ipcRenderer.on('auth:code', listener);
return () => ipcRenderer.removeListener('auth:code', listener);
},
// Dialoge / App
// MultiMC-Import
mmcPickFolder: () => ipcRenderer.invoke('mmc:pickFolder'),
mmcScan: (folder) => ipcRenderer.invoke('mmc:scan', folder),
mmcImport: (entries, copyData) => ipcRenderer.invoke('mmc:import', entries, copyData),
onMmcProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('mmc:progress', l);
return () => ipcRenderer.removeListener('mmc:progress', l);
},
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
onCfProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('cf:progress', l);
return () => ipcRenderer.removeListener('cf:progress', l);
},
mcPickFolder: () => ipcRenderer.invoke('mc:pickFolder'),
mcScan: (folder) => ipcRenderer.invoke('mc:scan', folder),
mcImport: (entries, copyData) => ipcRenderer.invoke('mc:import', entries, copyData),
onMcProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('mc:progress', l);
return () => ipcRenderer.removeListener('mc:progress', l);
},
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
onTechnicProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('technic:progress', l);
return () => ipcRenderer.removeListener('technic:progress', l);
},
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
onTechnicPlatformProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('technicPlatform:progress', l);
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
},
// Bedrock (Windows-Store-Ausgabe)
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
bedrockLaunch: () => ipcRenderer.invoke('bedrock:launch'),
bedrockWorlds: () => ipcRenderer.invoke('bedrock:worlds'),
bedrockList: (kind) => ipcRenderer.invoke('bedrock:list', kind),
bedrockRemove: (kind, name) => ipcRenderer.invoke('bedrock:remove', kind, name),
bedrockOpenFolder: (kind) => ipcRenderer.invoke('bedrock:openFolder', kind),
bedrockInstallAddon: () => ipcRenderer.invoke('bedrock:installAddon'),
bedrockOpenWorld: (dir) => ipcRenderer.invoke('bedrock:openWorld', dir),
// echte Minecraft-Icons
iconsList: () => ipcRenderer.invoke('icons:list'),
iconsExtract: () => ipcRenderer.invoke('icons:extract'),
// Updates
updateCheck: () => ipcRenderer.invoke('update:check'),
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
onUpdateAvailable: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:available', l);
return () => ipcRenderer.removeListener('update:available', l);
},
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
// verfügbarem Update feuert)
onUpdateChecked: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:checked', l);
return () => ipcRenderer.removeListener('update:checked', l);
},
onUpdateProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:progress', l);
return () => ipcRenderer.removeListener('update:progress', l);
},
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
appInfo: () => ipcRenderer.invoke('app:info'),
appLicense: () => ipcRenderer.invoke('app:license'),
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
popoutHide: () => ipcRenderer.send('popout:hide'),
onPopoutReopen: (cb) => {
const l = (_e, params) => cb(params);
ipcRenderer.on('popout:reopen', l);
return () => ipcRenderer.removeListener('popout:reopen', l);
},
onSettingsChanged: (cb) => {
const l = (_e, s) => cb(s);
ipcRenderer.on('settings:changed', l);
return () => ipcRenderer.removeListener('settings:changed', l);
},
onInstancesChanged: (cb) => {
const l = () => cb();
ipcRenderer.on('instances:changed', l);
return () => ipcRenderer.removeListener('instances:changed', l);
},
onSkinChanged: (cb) => {
const l = () => cb();
ipcRenderer.on('skin:changed', l);
return () => ipcRenderer.removeListener('skin:changed', l);
},
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
// Spielaufzeichnung
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
// Freundesystem
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
friendsList: () => ipcRenderer.invoke('friends:list'),
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
onFriendsRequestCount: (cb) => {
const l = (_e, zahl) => cb(zahl);
ipcRenderer.on('friends:requestCount', l);
return () => ipcRenderer.removeListener('friends:requestCount', l);
},
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
onRecordingServerDetected: (cb) => {
const l = (_e, payload) => cb(payload);
ipcRenderer.on('recording:serverDetected', l);
return () => ipcRenderer.removeListener('recording:serverDetected', l);
},
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
onGameAudioChunk: (cb) => {
const l = (_e, chunk) => cb(chunk);
ipcRenderer.on('recording:gameAudioChunk', l);
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
},
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
onRecordCloseAttempt: (cb) => {
const l = () => cb();
ipcRenderer.on('record:closeAttempt', l);
return () => ipcRenderer.removeListener('record:closeAttempt', l);
},
});
'use strict';
/*
* preload.js – stellt dem Renderer eine schmale, sichere API bereit.
* contextIsolation ist aktiv; der Renderer hat KEINEN direkten Node-Zugriff.
*/
const { contextBridge, ipcRenderer } = require('electron');
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
// damit man sie auch ohne offene DevTools sieht
window.addEventListener('error', (e) => {
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
});
window.addEventListener('unhandledrejection', (e) => {
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
});
contextBridge.exposeInMainWorld('api', {
// Einstellungen
getSettings: () => ipcRenderer.invoke('settings:get'),
saveSettings: (patch) => ipcRenderer.invoke('settings:save', patch),
getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'),
detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath),
scanJava: () => ipcRenderer.invoke('java:scan'),
listJava: () => ipcRenderer.invoke('java:list'),
// Minecraft-Versionen (Mojang)
mcVersions: () => ipcRenderer.invoke('mc:versions'),
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
// Mods (Modrinth)
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
curseforgeTestKey: (apiKey) => ipcRenderer.invoke('curseforge:testKey', apiKey),
// art: 'mod' | 'resourcepack' | 'shader'
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
systemMemory: () => ipcRenderer.invoke('system:memory'),
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
onAutoBackup: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('backup:auto', l);
return () => ipcRenderer.removeListener('backup:auto', l);
},
crashOpenFolder: (id) => ipcRenderer.invoke('crash:openFolder', id),
serversList: (id) => ipcRenderer.invoke('servers:list', id),
serversAdd: (id, eintrag) => ipcRenderer.invoke('servers:add', id, eintrag),
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
logsList: (id) => ipcRenderer.invoke('logs:list', id),
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
modCheckUpdates: (id, art) => ipcRenderer.invoke('mods:checkUpdates', id, art),
modApplyUpdates: (id, projectIds, art) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds, art),
onModUpdateProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('mods:updateProgress', l);
return () => ipcRenderer.removeListener('mods:updateProgress', l);
},
// Instanzen
listInstances: () => ipcRenderer.invoke('instances:list'),
getInstance: (id) => ipcRenderer.invoke('instances:get', id),
createInstance: (data) => ipcRenderer.invoke('instances:create', data),
installPerformancePack: (id) => ipcRenderer.invoke('instances:installPerformancePack', id),
updateInstance: (id, patch) => ipcRenderer.invoke('instances:update', id, patch),
deleteInstance: (id) => ipcRenderer.invoke('instances:delete', id),
duplicateInstance: (id) => ipcRenderer.invoke('instances:duplicate', id),
openInstanceFolder: (id) => ipcRenderer.invoke('instances:openFolder', id),
openGameDir: (id) => ipcRenderer.invoke('instances:openGameDir', id),
// Welten & Ressourcenpakete
worlds: (id) => ipcRenderer.invoke('instance:worlds', id),
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
openScreenshot: (id, name) => ipcRenderer.invoke('instance:openScreenshot', id, name),
launchInstance: (id, accountId, opts) => ipcRenderer.invoke('instances:launch', id, accountId, opts),
exportInstance: (id) => ipcRenderer.invoke('instances:export', id),
createGlobalBackup: () => ipcRenderer.invoke('backup:global'),
listGlobalBackups: () => ipcRenderer.invoke('backup:list'),
restoreGlobalBackup: (sourcePath) => ipcRenderer.invoke('backup:restore', sourcePath),
onBackupProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('backup:progress', l);
return () => ipcRenderer.removeListener('backup:progress', l);
},
importInstance: () => ipcRenderer.invoke('instances:import'),
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
onLaunchRequest: (cb) => {
const l = (_e, payload) => cb(payload);
ipcRenderer.on('instance:launch-request', l);
return () => ipcRenderer.removeListener('instance:launch-request', l);
},
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
notifyReady: () => ipcRenderer.send('renderer:ready'),
importModpack: () => ipcRenderer.invoke('modpack:import'),
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
onLaunchProgress: (cb) => {
const listener = (_e, data) => cb(data);
ipcRenderer.on('launch:progress', listener);
return () => ipcRenderer.removeListener('launch:progress', listener);
},
// eigenes Instanz-Bild
pickImage: () => ipcRenderer.invoke('dialog:pickImage'),
setInstanceImage: (id, sourcePath) => ipcRenderer.invoke('instances:setImage', id, sourcePath),
clearInstanceImage: (id) => ipcRenderer.invoke('instances:clearImage', id),
instanceIcon: (id) => ipcRenderer.invoke('instances:icon', id),
fileDataUri: (p) => ipcRenderer.invoke('file:dataUri', p),
// Microsoft-Login (Phase 3) + Mehrkonten
authStatus: () => ipcRenderer.invoke('auth:status'),
authAccounts: () => ipcRenderer.invoke('auth:accounts'),
authSetActive: (id) => ipcRenderer.invoke('auth:setActive', id),
skinProfile: () => ipcRenderer.invoke('skin:profile'),
skinChoose: () => ipcRenderer.invoke('skin:choose'),
skinApply: (pfad, variante) => ipcRenderer.invoke('skin:apply', pfad, variante),
skinReset: () => ipcRenderer.invoke('skin:reset'),
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
authLogin: () => ipcRenderer.invoke('auth:login'),
authLogout: () => ipcRenderer.invoke('auth:logout'),
authHealth: () => ipcRenderer.invoke('auth:health'),
authRecheck: () => ipcRenderer.invoke('auth:recheck'),
onAuthHealth: (cb) => {
const listener = (_e, data) => cb(data);
ipcRenderer.on('auth:health', listener);
return () => ipcRenderer.removeListener('auth:health', listener);
},
// meldet sich, wenn ein Spiel startet oder endet -> Kontenliste auffrischen
onAccountsChanged: (cb) => {
const listener = () => cb();
ipcRenderer.on('accounts:changed', listener);
return () => ipcRenderer.removeListener('accounts:changed', listener);
},
onAuthRefreshed: (cb) => {
const listener = () => cb();
ipcRenderer.on('auth:refreshed', listener);
return () => ipcRenderer.removeListener('auth:refreshed', listener);
},
onAuthCode: (cb) => {
const listener = (_e, data) => cb(data);
ipcRenderer.on('auth:code', listener);
return () => ipcRenderer.removeListener('auth:code', listener);
},
// Dialoge / App
// MultiMC-Import
mmcPickFolder: () => ipcRenderer.invoke('mmc:pickFolder'),
mmcScan: (folder) => ipcRenderer.invoke('mmc:scan', folder),
mmcImport: (entries, copyData) => ipcRenderer.invoke('mmc:import', entries, copyData),
onMmcProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('mmc:progress', l);
return () => ipcRenderer.removeListener('mmc:progress', l);
},
cfPickFolder: () => ipcRenderer.invoke('cf:pickFolder'),
cfScan: (folder) => ipcRenderer.invoke('cf:scan', folder),
cfImport: (entries, copyData) => ipcRenderer.invoke('cf:import', entries, copyData),
onCfProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('cf:progress', l);
return () => ipcRenderer.removeListener('cf:progress', l);
},
mcPickFolder: () => ipcRenderer.invoke('mc:pickFolder'),
mcScan: (folder) => ipcRenderer.invoke('mc:scan', folder),
mcImport: (entries, copyData) => ipcRenderer.invoke('mc:import', entries, copyData),
onMcProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('mc:progress', l);
return () => ipcRenderer.removeListener('mc:progress', l);
},
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
onTechnicProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('technic:progress', l);
return () => ipcRenderer.removeListener('technic:progress', l);
},
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
onTechnicPlatformProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('technicPlatform:progress', l);
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
},
// Bedrock (Windows-Store-Ausgabe)
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
bedrockLaunch: () => ipcRenderer.invoke('bedrock:launch'),
bedrockWorlds: () => ipcRenderer.invoke('bedrock:worlds'),
bedrockList: (kind) => ipcRenderer.invoke('bedrock:list', kind),
bedrockRemove: (kind, name) => ipcRenderer.invoke('bedrock:remove', kind, name),
bedrockOpenFolder: (kind) => ipcRenderer.invoke('bedrock:openFolder', kind),
bedrockInstallAddon: () => ipcRenderer.invoke('bedrock:installAddon'),
bedrockOpenWorld: (dir) => ipcRenderer.invoke('bedrock:openWorld', dir),
// echte Minecraft-Icons
iconsList: () => ipcRenderer.invoke('icons:list'),
iconsExtract: () => ipcRenderer.invoke('icons:extract'),
// Updates
updateCheck: () => ipcRenderer.invoke('update:check'),
updateLastCheck: () => ipcRenderer.invoke('update:lastCheck'),
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
onUpdateAvailable: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:available', l);
return () => ipcRenderer.removeListener('update:available', l);
},
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
// verfügbarem Update feuert)
onUpdateChecked: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:checked', l);
return () => ipcRenderer.removeListener('update:checked', l);
},
onUpdateProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:progress', l);
return () => ipcRenderer.removeListener('update:progress', l);
},
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
appInfo: () => ipcRenderer.invoke('app:info'),
appLicense: () => ipcRenderer.invoke('app:license'),
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
popoutHide: () => ipcRenderer.send('popout:hide'),
onPopoutReopen: (cb) => {
const l = (_e, params) => cb(params);
ipcRenderer.on('popout:reopen', l);
return () => ipcRenderer.removeListener('popout:reopen', l);
},
onSettingsChanged: (cb) => {
const l = (_e, s) => cb(s);
ipcRenderer.on('settings:changed', l);
return () => ipcRenderer.removeListener('settings:changed', l);
},
onInstancesChanged: (cb) => {
const l = () => cb();
ipcRenderer.on('instances:changed', l);
return () => ipcRenderer.removeListener('instances:changed', l);
},
onSkinChanged: (cb) => {
const l = () => cb();
ipcRenderer.on('skin:changed', l);
return () => ipcRenderer.removeListener('skin:changed', l);
},
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
// Spielaufzeichnung
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
recordingSave: (instanceName, buffer, serverLabel, fps) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel, fps),
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
recordingList: (instanceName) => ipcRenderer.invoke('recording:list', instanceName),
recordingThumbnail: (instanceName, dateiname) => ipcRenderer.invoke('recording:thumbnail', instanceName, dateiname),
recordingDelete: (instanceName, dateiname) => ipcRenderer.invoke('recording:delete', instanceName, dateiname),
recordingOpen: (instanceName, dateiname) => ipcRenderer.invoke('recording:open', instanceName, dateiname),
// Freundesystem
friendsEnabled: () => ipcRenderer.invoke('friends:enabled'),
friendsList: () => ipcRenderer.invoke('friends:list'),
friendsRequests: () => ipcRenderer.invoke('friends:requests'),
friendsAdd: (targetUsername) => ipcRenderer.invoke('friends:add', targetUsername),
friendsRespond: (otherUuid, accept) => ipcRenderer.invoke('friends:respond', otherUuid, accept),
friendsRemove: (otherUuid) => ipcRenderer.invoke('friends:remove', otherUuid),
friendsSentRequests: () => ipcRenderer.invoke('friends:sentRequests'),
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
clipSetActive: (active, instanceId) => ipcRenderer.invoke('clip:setActive', active, instanceId),
clipSave: (data) => ipcRenderer.invoke('clip:save', data),
onClipHotkey: (cb) => {
const l = () => cb();
ipcRenderer.on('clip:hotkey', l);
return () => ipcRenderer.removeListener('clip:hotkey', l);
},
onUiToast: (cb) => {
const l = (_e, t) => cb(t);
ipcRenderer.on('ui:toast', l);
return () => ipcRenderer.removeListener('ui:toast', l);
},
friendsJoin: (server, version) => ipcRenderer.invoke('friends:join', server, version),
friendsJoinNew: (server, version) => ipcRenderer.invoke('friends:joinNew', server, version),
friendsMyServer: () => ipcRenderer.invoke('friends:myServer'),
friendsInvite: (targetUuid) => ipcRenderer.invoke('friends:invite', targetUuid),
friendsInvites: () => ipcRenderer.invoke('friends:invites'),
friendsDismissInvite: (id) => ipcRenderer.invoke('friends:dismissInvite', id),
onFriendsInvite: (cb) => {
const l = (_e, einladung) => cb(einladung);
ipcRenderer.on('friends:invite', l);
return () => ipcRenderer.removeListener('friends:invite', l);
},
onFriendsRequestCount: (cb) => {
const l = (_e, zahl) => cb(zahl);
ipcRenderer.on('friends:requestCount', l);
return () => ipcRenderer.removeListener('friends:requestCount', l);
},
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
onRecordingServerDetected: (cb) => {
const l = (_e, payload) => cb(payload);
ipcRenderer.on('recording:serverDetected', l);
return () => ipcRenderer.removeListener('recording:serverDetected', l);
},
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
onGameAudioChunk: (cb) => {
const l = (_e, chunk) => cb(chunk);
ipcRenderer.on('recording:gameAudioChunk', l);
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
},
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
onRecordCloseAttempt: (cb) => {
const l = () => cb();
ipcRenderer.on('record:closeAttempt', l);
return () => ipcRenderer.removeListener('record:closeAttempt', l);
},
});
+5955 -5607
View File
File diff suppressed because it is too large Load Diff
+500 -495
View File
@@ -1,495 +1,500 @@
'use strict';
/*
* store.js – Persistenz für AeroMC
* ------------------------------------
* Jede Instanz ist ein isolierter Ordner (MultiMC-Prinzip):
* <instancesDir>/<id>/instance.json – Metadaten
* <instancesDir>/<id>/minecraft/ – das eigentliche Spielverzeichnis (.minecraft-Ersatz)
*
* Globale Einstellungen liegen in <userData>/settings.json.
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
let USER_DATA = null; // wird von main.js via init() gesetzt
function init(userDataPath) {
USER_DATA = userDataPath;
ensureDir(USER_DATA);
ensureDir(getInstancesDir());
}
// ---------- kleine Helfer ----------
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function readJson(file, fallback) {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'));
} catch {
return fallback;
}
}
function writeJson(file, data) {
ensureDir(path.dirname(file));
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
}
function newId() {
return crypto.randomBytes(8).toString('hex');
}
// ---------- Einstellungen ----------
const DEFAULT_SETTINGS = {
instancesDir: null, // null => Standard (userData/instances)
javaPath: '', // leer => "java" aus PATH
autoJava: true, // fehlende Java-Version bei Bedarf selbst herunterladen
autoBackupEnabled: false, // Sicherungen nach Zeitplan – standardmäßig aus
autoBackupEvery: 'daily', // 'daily' | 'weekly'
autoBackupBeforeLaunch: false, // zusätzlich vor jedem Spielstart
autoBackupKeep: 5, // wie viele automatische Sicherungen behalten werden
autoBackupLast: '', // Zeitpunkt der letzten automatischen Sicherung
defaultMinMemMb: 2048,
defaultMaxMemMb: 4096,
azureClientId: '', // Azure-App-ID für den Microsoft-Login (Phase 3)
curseforgeApiKey: '', // eigener CurseForge-API-Key (hat Vorrang)
curseforgeProxyUrl: '', // sonst: Adresse der eigenen CurseForge-Weiterleitung
curseforgeProxyToken: '', // optionales Geheimnis für diese Weiterleitung
globalPreLaunch: '',
globalPostExit: '',
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
showConsoleOnLaunch: false, // Konsole beim Spielstart anzeigen
autoCloseConsole: false, // Konsole nach Spielende automatisch schließen
showConsoleOnCrash: true, // Konsole anzeigen, wenn das Spiel abstürzt
consoleMaxLines: 100000, // Zeilenbegrenzung der Konsole
mcMaximized: false, // Minecraft maximiert starten
mcWidth: 854,
mcHeight: 480,
sortBy: 'lastPlayed', // lastPlayed | name
iso3dIcons: true, // Block-Symbole räumlich darstellen
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
friendsEnabled: true, // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
proxyHost: '',
proxyPort: '',
proxyUser: '',
proxyPass: '',
theme: 'dark',
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
recordingResolution: 'native',
recordingQuality: 'medium',
recordingFramerate: 30,
recordingAudioSystem: true,
recordingAudioGame: false,
recordingVolSystem: 100,
recordingVolGame: 100,
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
};
function settingsFile() {
return path.join(USER_DATA, 'settings.json');
}
function getSettings() {
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
}
function saveSettings(patch) {
const merged = Object.assign({}, getSettings(), patch || {});
writeJson(settingsFile(), merged);
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
ensureDir(getInstancesDir());
return merged;
}
// ---------- Instanzen ----------
function getInstancesDir() {
const s = getSettings();
return s.instancesDir && s.instancesDir.trim()
? s.instancesDir
: path.join(USER_DATA, 'instances');
}
function instancePath(id) {
return path.join(getInstancesDir(), id);
}
function instanceFile(id) {
return path.join(instancePath(id), 'instance.json');
}
function gameDir(id) {
return path.join(instancePath(id), 'minecraft');
}
/*
* Inhalte je Art: Mods, Ressourcenpakete und Shader liegen in verschiedenen
* Ordnern des Spielverzeichnisses und werden getrennt verzeichnet.
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
* weiterlaufen.
*/
const INHALT_ORDNER = {
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
};
const INHALT_INDEX = {
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
};
function inhaltsDir(id, art = 'mod') {
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
}
function modsDir(id, art = 'mod') {
return inhaltsDir(id, art);
}
function modsIndexFile(id, art = 'mod') {
return path.join(instancePath(id), INHALT_INDEX[art] || INHALT_INDEX.mod);
}
// ---------- installierte Mods (Metadaten je Instanz) ----------
/*
* Abgeschaltete Mods heißen "<name>.jar.disabled" – so übergeht Minecraft sie,
* ohne dass die Datei verloren geht. In mods.json steht immer der eingeschaltete
* Name; ob eine Mod an ist, entscheidet allein die Datei auf der Platte.
*/
const MOD_ENDUNG = /\.(jar|zip)$/i;
const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i;
function scanModsDir(id, art = 'mod') {
const dir = modsDir(id, art);
if (!fs.existsSync(dir)) return [];
const gefunden = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
const aus = AUS_ENDUNG.test(entry.name);
if (!aus && !MOD_ENDUNG.test(entry.name)) continue;
const filename = aus ? entry.name.replace(/\.disabled$/i, '') : entry.name;
gefunden.push({
projectId: null,
title: filename.replace(MOD_ENDUNG, ''),
filename,
versionNumber: 'Importiert',
enabled: !aus,
});
}
gefunden.sort((a, b) => a.filename.localeCompare(b.filename));
return gefunden;
}
function listMods(id, art = 'mod') {
const aufDerPlatte = new Map(scanModsDir(id, art).map((m) => [m.filename, m]));
const indexed = readJson(modsIndexFile(id, art), []);
const merged = new Map();
for (const entry of indexed) {
if (!entry || !entry.filename) continue;
const platte = aufDerPlatte.get(entry.filename);
merged.set(entry.filename, Object.assign({}, entry, {
enabled: platte ? platte.enabled : true,
fehlt: !platte, // steht im Verzeichnis, Datei ist aber weg
}));
}
for (const [name, entry] of aufDerPlatte) {
if (!merged.has(name)) merged.set(name, Object.assign({ fehlt: false }, entry));
}
return [...merged.values()];
}
/*
* Schaltet eine Mod an oder aus, indem die Datei umbenannt wird.
* -> { ok, enabled } oder { ok: false, reason }
*/
function setModEnabled(id, filename, enabled, art = 'mod') {
const dir = modsDir(id, art);
const anPfad = path.join(dir, filename);
const ausPfad = anPfad + '.disabled';
// Pfad darf den mods-Ordner nicht verlassen
if (path.dirname(anPfad) !== dir) return { ok: false, reason: 'bad-name' };
const vonPfad = enabled ? ausPfad : anPfad;
const nachPfad = enabled ? anPfad : ausPfad;
if (fs.existsSync(anPfad) && fs.existsSync(ausPfad)) {
// beide Fassungen da – nichts anfassen, sonst geht eine verloren
return { ok: false, reason: 'both-exist' };
}
if (!fs.existsSync(vonPfad)) {
if (fs.existsSync(nachPfad)) return { ok: true, enabled, unveraendert: true };
return { ok: false, reason: 'not-found' };
}
try {
fs.renameSync(vonPfad, nachPfad);
return { ok: true, enabled };
} catch (err) {
return { ok: false, reason: err.message };
}
}
function addMod(id, entry, art = 'mod') {
const list = listMods(id, art).filter((m) => m.filename !== entry.filename);
list.push(entry);
writeJson(modsIndexFile(id, art), list);
return list;
}
/*
* Findet installierte Mods, die den per "filename" identifizierten Mod als
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
* können aber selbst weiterhin als Abhängiger auftauchen).
* -> [Titel, Titel, ...] der abhängigen Mods
*/
function findDependents(id, filename, art = 'mod') {
const list = listMods(id, art);
const target = list.find((m) => m.filename === filename);
if (!target || !target.projectId) return [];
return list
.filter((m) => m.filename !== filename
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
.map((m) => m.title || m.filename);
}
function removeMod(id, filename, art = 'mod') {
const list = listMods(id, art).filter((m) => m.filename !== filename);
writeJson(modsIndexFile(id, art), list);
// zugehörige Datei löschen – auch, wenn sie gerade abgeschaltet ist
const jar = path.join(modsDir(id, art), filename);
for (const p of [jar, jar + '.disabled']) {
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ }
}
return list;
}
function defaultInstance(name) {
const now = new Date().toISOString();
return {
id: newId(),
name: name || 'Neue Instanz',
icon: 'grass', // Schlüssel für ein Emoji/Icon im Renderer
customImage: null, // Dateiname eines eigenen Bildes im Instanzordner (überschreibt Emoji)
group: '', // optionale Gruppe (MultiMC-artig)
preLaunchCommand: '', // Befehl vor dem Start
postExitCommand: '', // Befehl nach dem Beenden
created: now,
lastPlayed: null,
totalPlaySeconds: 0,
minecraft: {
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
loaderVersion: '',
},
java: {
path: '', // leer => globale Einstellung
minMemMb: null, // null => globale Einstellung
maxMemMb: null,
extraArgs: '',
},
notes: '',
};
}
// liest ein eigenes Instanz-Bild als data:-URI (für die Anzeige im Renderer)
function iconDataUri(id) {
const meta = readJson(instanceFile(id), null);
if (!meta || !meta.customImage) return null;
try {
const buf = fs.readFileSync(path.join(instancePath(id), meta.customImage));
let ext = path.extname(meta.customImage).slice(1).toLowerCase();
if (ext === 'jpg') ext = 'jpeg';
return `data:image/${ext};base64,` + buf.toString('base64');
} catch { return null; }
}
function setCustomImage(id, sourcePath) {
let ext = path.extname(sourcePath).toLowerCase();
if (!ext) ext = '.png';
const dest = 'icon' + ext;
fs.copyFileSync(sourcePath, path.join(instancePath(id), dest));
return updateInstance(id, { customImage: dest });
}
function clearCustomImage(id) {
const meta = getInstance(id);
if (meta && meta.customImage) {
try { fs.unlinkSync(path.join(instancePath(id), meta.customImage)); } catch { /* egal */ }
}
return updateInstance(id, { customImage: null });
}
function listInstances() {
const dir = getInstancesDir();
ensureDir(dir);
const result = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const meta = readJson(path.join(dir, entry.name, 'instance.json'), null);
if (meta && meta.id) {
// eigenes Bild direkt für die Anzeige mitliefern (nicht persistiert)
if (meta.customImage) meta.iconData = iconDataUri(meta.id);
result.push(meta);
}
}
// Sortierung laut Einstellung: zuletzt gespielt oder nach Name
const byName = getSettings().sortBy === 'name';
result.sort((a, b) => {
if (byName) return a.name.localeCompare(b.name);
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
if (a.lastPlayed) return -1;
if (b.lastPlayed) return 1;
return a.name.localeCompare(b.name);
});
return result;
}
function getInstance(id) {
return readJson(instanceFile(id), null);
}
function normalizeInstanceName(name) {
return String(name || '').trim().toLowerCase();
}
function findInstanceByName(name) {
const needle = normalizeInstanceName(name);
if (!needle) return null;
return listInstances().find((inst) => normalizeInstanceName(inst.name) === needle) || null;
}
// Ordnername aus dem Instanznamen ableiten (wie MultiMC), ungültige Zeichen raus
function slugForName(name) {
const base = String(name || 'Instanz').trim()
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
.replace(/[.\s]+$/, '')
.trim();
return (base || 'Instanz').slice(0, 60);
}
// eindeutiger Ordnername (bei Kollision "Name 2", "Name 3", …)
function uniqueFolderId(name) {
const dir = getInstancesDir();
const base = slugForName(name);
let id = base;
let n = 2;
while (fs.existsSync(path.join(dir, id))) { id = base + ' ' + n; n += 1; }
return id;
}
function applyInstanceData(inst, data) {
if (!data) return inst;
if (data.name) inst.name = data.name;
if (data.icon) inst.icon = data.icon;
if (data.minecraft) Object.assign(inst.minecraft, data.minecraft);
if (data.notes !== undefined) inst.notes = data.notes;
if (data.group !== undefined) inst.group = data.group;
return inst;
}
function writeInstance(inst) {
ensureDir(gameDir(inst.id));
writeJson(instanceFile(inst.id), inst);
return inst;
}
function createInstance(data) {
const inst = applyInstanceData(defaultInstance(data && data.name), data);
inst.id = uniqueFolderId(inst.name); // Ordner = Instanzname statt Zufalls-ID
return writeInstance(inst);
}
function createOrReplaceImportedInstance(data) {
const existing = findInstanceByName(data && data.name);
if (!existing) return createInstance(data);
const inst = applyInstanceData(defaultInstance(data && data.name), data);
inst.id = existing.id;
inst.created = existing.created || inst.created;
inst.lastPlayed = existing.lastPlayed || null;
inst.totalPlaySeconds = existing.totalPlaySeconds || 0;
const p = instancePath(existing.id);
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
return writeInstance(inst);
}
function updateInstance(id, patch) {
const inst = getInstance(id);
if (!inst) throw new Error('Instanz nicht gefunden: ' + id);
// flaches Merge mit Sonderbehandlung der verschachtelten Objekte
const next = Object.assign({}, inst, patch);
if (patch.minecraft) next.minecraft = Object.assign({}, inst.minecraft, patch.minecraft);
if (patch.java) next.java = Object.assign({}, inst.java, patch.java);
writeJson(instanceFile(id), next);
return next;
}
function deleteInstance(id) {
const p = instancePath(id);
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
return true;
}
function duplicateInstance(id) {
const src = getInstance(id);
if (!src) throw new Error('Instanz nicht gefunden: ' + id);
const copy = JSON.parse(JSON.stringify(src));
copy.name = src.name + ' (Kopie)';
copy.id = uniqueFolderId(copy.name);
copy.created = new Date().toISOString();
copy.lastPlayed = null;
copy.totalPlaySeconds = 0;
ensureDir(gameDir(copy.id));
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
try {
fs.cpSync(gameDir(id), gameDir(copy.id), { recursive: true });
} catch { /* Quelle evtl. leer */ }
writeJson(instanceFile(copy.id), copy);
return copy;
}
module.exports = {
init,
getSettings,
saveSettings,
getInstancesDir,
instancePath,
gameDir,
modsDir,
inhaltsDir,
iconDataUri,
setCustomImage,
clearCustomImage,
listMods,
addMod,
removeMod,
findDependents,
setModEnabled,
listInstances,
getInstance,
findInstanceByName,
createInstance,
createOrReplaceImportedInstance,
updateInstance,
deleteInstance,
duplicateInstance,
};
'use strict';
/*
* store.js – Persistenz für AeroMC
* ------------------------------------
* Jede Instanz ist ein isolierter Ordner (MultiMC-Prinzip):
* <instancesDir>/<id>/instance.json – Metadaten
* <instancesDir>/<id>/minecraft/ – das eigentliche Spielverzeichnis (.minecraft-Ersatz)
*
* Globale Einstellungen liegen in <userData>/settings.json.
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
let USER_DATA = null; // wird von main.js via init() gesetzt
function init(userDataPath) {
USER_DATA = userDataPath;
ensureDir(USER_DATA);
ensureDir(getInstancesDir());
}
// ---------- kleine Helfer ----------
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function readJson(file, fallback) {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'));
} catch {
return fallback;
}
}
function writeJson(file, data) {
ensureDir(path.dirname(file));
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
}
function newId() {
return crypto.randomBytes(8).toString('hex');
}
// ---------- Einstellungen ----------
const DEFAULT_SETTINGS = {
instancesDir: null, // null => Standard (userData/instances)
javaPath: '', // leer => "java" aus PATH
autoJava: true, // fehlende Java-Version bei Bedarf selbst herunterladen
autoBackupEnabled: false, // Sicherungen nach Zeitplan – standardmäßig aus
autoBackupEvery: 'daily', // 'daily' | 'weekly'
autoBackupBeforeLaunch: false, // zusätzlich vor jedem Spielstart
autoBackupKeep: 5, // wie viele automatische Sicherungen behalten werden
autoBackupLast: '', // Zeitpunkt der letzten automatischen Sicherung
defaultMinMemMb: 2048,
defaultMaxMemMb: 4096,
azureClientId: '', // Azure-App-ID für den Microsoft-Login (Phase 3)
curseforgeApiKey: '', // eigener CurseForge-API-Key (hat Vorrang)
curseforgeProxyUrl: '', // sonst: Adresse der eigenen CurseForge-Weiterleitung
curseforgeProxyToken: '', // optionales Geheimnis für diese Weiterleitung
globalPreLaunch: '',
globalPostExit: '',
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
showConsoleOnLaunch: false, // Konsole beim Spielstart anzeigen
autoCloseConsole: false, // Konsole nach Spielende automatisch schließen
showConsoleOnCrash: true, // Konsole anzeigen, wenn das Spiel abstürzt
consoleMaxLines: 100000, // Zeilenbegrenzung der Konsole
mcMaximized: false, // Minecraft maximiert starten
mcWidth: 854,
mcHeight: 480,
sortBy: 'lastPlayed', // lastPlayed | name
iso3dIcons: true, // Block-Symbole räumlich darstellen
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
friendsEnabled: true,
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
proxyHost: '',
proxyPort: '',
proxyUser: '',
proxyPass: '',
theme: 'dark',
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
recordingResolution: 'native',
recordingQuality: 'medium',
recordingFramerate: 30,
recordingAudioSystem: true,
recordingAudioGame: false,
recordingVolSystem: 100,
recordingVolGame: 100,
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
};
function settingsFile() {
return path.join(USER_DATA, 'settings.json');
}
function getSettings() {
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
}
function saveSettings(patch) {
const merged = Object.assign({}, getSettings(), patch || {});
writeJson(settingsFile(), merged);
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
ensureDir(getInstancesDir());
return merged;
}
// ---------- Instanzen ----------
function getInstancesDir() {
const s = getSettings();
return s.instancesDir && s.instancesDir.trim()
? s.instancesDir
: path.join(USER_DATA, 'instances');
}
function instancePath(id) {
return path.join(getInstancesDir(), id);
}
function instanceFile(id) {
return path.join(instancePath(id), 'instance.json');
}
function gameDir(id) {
return path.join(instancePath(id), 'minecraft');
}
/*
* Inhalte je Art: Mods, Ressourcenpakete und Shader liegen in verschiedenen
* Ordnern des Spielverzeichnisses und werden getrennt verzeichnet.
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
* weiterlaufen.
*/
const INHALT_ORDNER = {
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
};
const INHALT_INDEX = {
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
};
function inhaltsDir(id, art = 'mod') {
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
}
function modsDir(id, art = 'mod') {
return inhaltsDir(id, art);
}
function modsIndexFile(id, art = 'mod') {
return path.join(instancePath(id), INHALT_INDEX[art] || INHALT_INDEX.mod);
}
// ---------- installierte Mods (Metadaten je Instanz) ----------
/*
* Abgeschaltete Mods heißen "<name>.jar.disabled" – so übergeht Minecraft sie,
* ohne dass die Datei verloren geht. In mods.json steht immer der eingeschaltete
* Name; ob eine Mod an ist, entscheidet allein die Datei auf der Platte.
*/
const MOD_ENDUNG = /\.(jar|zip)$/i;
const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i;
function scanModsDir(id, art = 'mod') {
const dir = modsDir(id, art);
if (!fs.existsSync(dir)) return [];
const gefunden = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
const aus = AUS_ENDUNG.test(entry.name);
if (!aus && !MOD_ENDUNG.test(entry.name)) continue;
const filename = aus ? entry.name.replace(/\.disabled$/i, '') : entry.name;
gefunden.push({
projectId: null,
title: filename.replace(MOD_ENDUNG, ''),
filename,
versionNumber: 'Importiert',
enabled: !aus,
});
}
gefunden.sort((a, b) => a.filename.localeCompare(b.filename));
return gefunden;
}
function listMods(id, art = 'mod') {
const aufDerPlatte = new Map(scanModsDir(id, art).map((m) => [m.filename, m]));
const indexed = readJson(modsIndexFile(id, art), []);
const merged = new Map();
for (const entry of indexed) {
if (!entry || !entry.filename) continue;
const platte = aufDerPlatte.get(entry.filename);
merged.set(entry.filename, Object.assign({}, entry, {
enabled: platte ? platte.enabled : true,
fehlt: !platte, // steht im Verzeichnis, Datei ist aber weg
}));
}
for (const [name, entry] of aufDerPlatte) {
if (!merged.has(name)) merged.set(name, Object.assign({ fehlt: false }, entry));
}
return [...merged.values()];
}
/*
* Schaltet eine Mod an oder aus, indem die Datei umbenannt wird.
* -> { ok, enabled } oder { ok: false, reason }
*/
function setModEnabled(id, filename, enabled, art = 'mod') {
const dir = modsDir(id, art);
const anPfad = path.join(dir, filename);
const ausPfad = anPfad + '.disabled';
// Pfad darf den mods-Ordner nicht verlassen
if (path.dirname(anPfad) !== dir) return { ok: false, reason: 'bad-name' };
const vonPfad = enabled ? ausPfad : anPfad;
const nachPfad = enabled ? anPfad : ausPfad;
if (fs.existsSync(anPfad) && fs.existsSync(ausPfad)) {
// beide Fassungen da – nichts anfassen, sonst geht eine verloren
return { ok: false, reason: 'both-exist' };
}
if (!fs.existsSync(vonPfad)) {
if (fs.existsSync(nachPfad)) return { ok: true, enabled, unveraendert: true };
return { ok: false, reason: 'not-found' };
}
try {
fs.renameSync(vonPfad, nachPfad);
return { ok: true, enabled };
} catch (err) {
return { ok: false, reason: err.message };
}
}
function addMod(id, entry, art = 'mod') {
const list = listMods(id, art).filter((m) => m.filename !== entry.filename);
list.push(entry);
writeJson(modsIndexFile(id, art), list);
return list;
}
/*
* Findet installierte Mods, die den per "filename" identifizierten Mod als
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
* können aber selbst weiterhin als Abhängiger auftauchen).
* -> [Titel, Titel, ...] der abhängigen Mods
*/
function findDependents(id, filename, art = 'mod') {
const list = listMods(id, art);
const target = list.find((m) => m.filename === filename);
if (!target || !target.projectId) return [];
return list
.filter((m) => m.filename !== filename
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
.map((m) => m.title || m.filename);
}
function removeMod(id, filename, art = 'mod') {
const list = listMods(id, art).filter((m) => m.filename !== filename);
writeJson(modsIndexFile(id, art), list);
// zugehörige Datei löschen – auch, wenn sie gerade abgeschaltet ist
const jar = path.join(modsDir(id, art), filename);
for (const p of [jar, jar + '.disabled']) {
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ }
}
return list;
}
function defaultInstance(name) {
const now = new Date().toISOString();
return {
id: newId(),
name: name || 'Neue Instanz',
icon: 'grass', // Schlüssel für ein Emoji/Icon im Renderer
customImage: null, // Dateiname eines eigenen Bildes im Instanzordner (überschreibt Emoji)
group: '', // optionale Gruppe (MultiMC-artig)
preLaunchCommand: '', // Befehl vor dem Start
postExitCommand: '', // Befehl nach dem Beenden
created: now,
lastPlayed: null,
totalPlaySeconds: 0,
launchCount: 0, // Anzahl Spielstarts (Spielzeit-Statistik)
minecraft: {
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
loaderVersion: '',
},
java: {
path: '', // leer => globale Einstellung
minMemMb: null, // null => globale Einstellung
maxMemMb: null,
extraArgs: '',
},
notes: '',
};
}
// liest ein eigenes Instanz-Bild als data:-URI (für die Anzeige im Renderer)
function iconDataUri(id) {
const meta = readJson(instanceFile(id), null);
if (!meta || !meta.customImage) return null;
try {
const buf = fs.readFileSync(path.join(instancePath(id), meta.customImage));
let ext = path.extname(meta.customImage).slice(1).toLowerCase();
if (ext === 'jpg') ext = 'jpeg';
return `data:image/${ext};base64,` + buf.toString('base64');
} catch { return null; }
}
function setCustomImage(id, sourcePath) {
let ext = path.extname(sourcePath).toLowerCase();
if (!ext) ext = '.png';
const dest = 'icon' + ext;
fs.copyFileSync(sourcePath, path.join(instancePath(id), dest));
return updateInstance(id, { customImage: dest });
}
function clearCustomImage(id) {
const meta = getInstance(id);
if (meta && meta.customImage) {
try { fs.unlinkSync(path.join(instancePath(id), meta.customImage)); } catch { /* egal */ }
}
return updateInstance(id, { customImage: null });
}
function listInstances() {
const dir = getInstancesDir();
ensureDir(dir);
const result = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const meta = readJson(path.join(dir, entry.name, 'instance.json'), null);
if (meta && meta.id) {
// eigenes Bild direkt für die Anzeige mitliefern (nicht persistiert)
if (meta.customImage) meta.iconData = iconDataUri(meta.id);
result.push(meta);
}
}
// Sortierung laut Einstellung: zuletzt gespielt oder nach Name
const byName = getSettings().sortBy === 'name';
result.sort((a, b) => {
if (byName) return a.name.localeCompare(b.name);
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
if (a.lastPlayed) return -1;
if (b.lastPlayed) return 1;
return a.name.localeCompare(b.name);
});
return result;
}
function getInstance(id) {
return readJson(instanceFile(id), null);
}
function normalizeInstanceName(name) {
return String(name || '').trim().toLowerCase();
}
function findInstanceByName(name) {
const needle = normalizeInstanceName(name);
if (!needle) return null;
return listInstances().find((inst) => normalizeInstanceName(inst.name) === needle) || null;
}
// Ordnername aus dem Instanznamen ableiten (wie MultiMC), ungültige Zeichen raus
function slugForName(name) {
const base = String(name || 'Instanz').trim()
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
.replace(/[.\s]+$/, '')
.trim();
return (base || 'Instanz').slice(0, 60);
}
// eindeutiger Ordnername (bei Kollision "Name 2", "Name 3", …)
function uniqueFolderId(name) {
const dir = getInstancesDir();
const base = slugForName(name);
let id = base;
let n = 2;
while (fs.existsSync(path.join(dir, id))) { id = base + ' ' + n; n += 1; }
return id;
}
function applyInstanceData(inst, data) {
if (!data) return inst;
if (data.name) inst.name = data.name;
if (data.icon) inst.icon = data.icon;
if (data.minecraft) Object.assign(inst.minecraft, data.minecraft);
if (data.notes !== undefined) inst.notes = data.notes;
if (data.group !== undefined) inst.group = data.group;
return inst;
}
function writeInstance(inst) {
ensureDir(gameDir(inst.id));
writeJson(instanceFile(inst.id), inst);
return inst;
}
function createInstance(data) {
const inst = applyInstanceData(defaultInstance(data && data.name), data);
inst.id = uniqueFolderId(inst.name); // Ordner = Instanzname statt Zufalls-ID
return writeInstance(inst);
}
function createOrReplaceImportedInstance(data) {
const existing = findInstanceByName(data && data.name);
if (!existing) return createInstance(data);
const inst = applyInstanceData(defaultInstance(data && data.name), data);
inst.id = existing.id;
inst.created = existing.created || inst.created;
inst.lastPlayed = existing.lastPlayed || null;
inst.totalPlaySeconds = existing.totalPlaySeconds || 0;
inst.launchCount = existing.launchCount || 0;
const p = instancePath(existing.id);
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
return writeInstance(inst);
}
function updateInstance(id, patch) {
const inst = getInstance(id);
if (!inst) throw new Error('Instanz nicht gefunden: ' + id);
// flaches Merge mit Sonderbehandlung der verschachtelten Objekte
const next = Object.assign({}, inst, patch);
if (patch.minecraft) next.minecraft = Object.assign({}, inst.minecraft, patch.minecraft);
if (patch.java) next.java = Object.assign({}, inst.java, patch.java);
writeJson(instanceFile(id), next);
return next;
}
function deleteInstance(id) {
const p = instancePath(id);
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
return true;
}
function duplicateInstance(id) {
const src = getInstance(id);
if (!src) throw new Error('Instanz nicht gefunden: ' + id);
const copy = JSON.parse(JSON.stringify(src));
copy.name = src.name + ' (Kopie)';
copy.id = uniqueFolderId(copy.name);
copy.created = new Date().toISOString();
copy.lastPlayed = null;
copy.totalPlaySeconds = 0;
copy.launchCount = 0;
ensureDir(gameDir(copy.id));
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
try {
fs.cpSync(gameDir(id), gameDir(copy.id), { recursive: true });
} catch { /* Quelle evtl. leer */ }
writeJson(instanceFile(copy.id), copy);
return copy;
}
module.exports = {
init,
getSettings,
saveSettings,
getInstancesDir,
instancePath,
gameDir,
modsDir,
inhaltsDir,
iconDataUri,
setCustomImage,
clearCustomImage,
listMods,
addMod,
removeMod,
findDependents,
setModEnabled,
listInstances,
getInstance,
findInstanceByName,
createInstance,
createOrReplaceImportedInstance,
updateInstance,
deleteInstance,
duplicateInstance,
};
+36
View File
@@ -391,6 +391,42 @@ img.account-av{object-fit:cover;image-rendering:auto}
.tb-badge{display:inline-block;min-width:16px;height:16px;padding:0 4px;margin-left:6px;border-radius:8px;background:#d9453c;color:#fff;font-size:11px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
#modal-friends .field-row{margin-bottom:14px}
#modal-friends .field-row input{flex:1}
/* ---- Spielzeit-Statistik: Kennzahlen + Balken pro Instanz ---- */
:root{--stat-bar:#3a6ea5}
:root[data-theme="dark"]{--stat-bar:#3d8fe0}
.stats-tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:6px}
.stat-tile{border:1px solid var(--border);border-radius:4px;padding:8px 10px;min-width:0}
.stat-tile .stat-label{font-size:11px;color:var(--dim)}
.stat-tile .stat-value{font-size:18px;font-weight:600;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stat-tile .stat-sub{font-size:11px;color:var(--dim);line-height:1.35}
.stats-list{display:flex;flex-direction:column;gap:2px}
.stat-row{display:grid;grid-template-columns:minmax(0,38%) 1fr auto;align-items:center;gap:10px;padding:5px 4px;border-radius:3px}
.stat-row:hover{background:var(--sel)}
.stat-name{min-width:0}
.stat-name b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stat-name span{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
.stat-track{height:8px}
.stat-bar{height:8px;background:var(--stat-bar);border-radius:0 4px 4px 0;min-width:2px}
.stat-value-cell{font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right;min-width:92px}
.popout-host .stats-list{max-height:none}
/* Clip-Modus-Leiste im Aufzeichnungsfenster */
.clip-bar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:8px 12px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
.clip-bar.aktiv{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}
.clip-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;font-weight:600;user-select:none}
.clip-switch input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
.clip-switch-track{position:relative;width:32px;height:18px;border-radius:9px;background:var(--border);transition:background .15s}
.clip-switch-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:left .15s}
.clip-switch input:checked + .clip-switch-track{background:var(--accent)}
.clip-switch input:checked + .clip-switch-track .clip-switch-knob{left:16px}
.clip-switch input:focus-visible + .clip-switch-track{outline:2px solid var(--accent);outline-offset:2px}
.clip-length{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dim)}
.clip-length select{padding:3px 6px;border:1px solid var(--border);border-radius:3px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:12px}
.clip-length select:disabled{opacity:.6}
.clip-status{flex:1;min-width:200px;font-size:12px;color:var(--dim)}
.clip-bar.aktiv .clip-status{color:var(--text)}
.clip-bar.aktiv .clip-status::before{content:"";display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:var(--danger);vertical-align:1px;animation:rec-pulse 1.6s infinite}
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
+4 -3
View File
@@ -42,7 +42,7 @@ function computeInstallKind() {
// 2. Notnagel über den Dateinamen, falls die Variablen einmal fehlen
if (/portable/i.test(path.basename(exe))) return 'portable';
// 3. Setup: NSIS legt neben der EXE immer einen Uninstaller ab. Nur wenn der
// 3. Setup: das Setup legt neben der EXE immer "Uninstall AeroMC.exe" ab. Nur wenn der
// da ist, ist es wirklich eine Installation.
if (hasUninstaller(path.dirname(exe))) return 'setup';
@@ -319,7 +319,7 @@ exit /b 1
/*
* Wendet das geladene Update an.
* setup -> NSIS-Installer starten, er überschreibt die Installation selbst
* setup -> AeroMC-Setup mit --update starten, es überschreibt die Installation selbst
* portable -> Skript starten, das die laufende EXE nach dem Beenden ersetzt
* Der Launcher muss sich danach beenden.
*/
@@ -331,7 +331,8 @@ function applyUpdate(file) {
const kind = installKind;
if (kind !== 'portable') {
const child = spawn(file.path, [], { detached: true, stdio: 'ignore' });
// --update: das AeroMC-Setup aktualisiert ohne Rückfragen und startet AeroMC danach neu
const child = spawn(file.path, ['--update'], { detached: true, stdio: 'ignore' });
child.unref();
return { started: 'installer' };
}