Add launcher catalog workflow and smoke tests
Some checks failed
Build / release (macos-latest) (push) Has been cancelled
Build / release (ubuntu-latest) (push) Has been cancelled
Build / release (windows-latest) (push) Has been cancelled
Windows Smoke Test / windows-smoke (push) Has been cancelled

This commit is contained in:
2026-05-04 14:06:05 +09:00
parent eb7ef9bbf2
commit 24a0569fb4
106 changed files with 24095 additions and 6 deletions

View File

@@ -0,0 +1,168 @@
const AdmZip = require('adm-zip')
const fs = require('fs-extra')
const got = require('got')
const os = require('os')
const path = require('path')
const { pipeline } = require('stream/promises')
const ConfigManager = require('./configmanager')
function getProfileBaseDirectory(profileId){
return path.join(ConfigManager.getDataDirectory(), 'profiles', profileId)
}
function getProfileDownloadDirectory(profileId){
return path.join(getProfileBaseDirectory(profileId), 'downloads')
}
function getProfileCacheFile(profileId, fileName){
return path.join(getProfileDownloadDirectory(profileId), fileName)
}
function getServerBundleDirectory(profile){
return path.join(getProfileBaseDirectory(profile.id), profile.serverDirectoryName || 'server')
}
function isRemoteSource(source){
return /^https?:\/\//i.test(source)
}
function resolveLocalSource(source){
if(source == null){
return null
}
if(source.startsWith('file://')){
return decodeURIComponent(source.substring('file://'.length))
}
return path.resolve(source)
}
async function downloadSourceToFile(source, destination){
await fs.ensureDir(path.dirname(destination))
if(isRemoteSource(source)){
await pipeline(
got.stream(source),
fs.createWriteStream(destination)
)
return destination
}
const localSource = resolveLocalSource(source)
const localStat = await fs.stat(localSource)
if(localStat.isDirectory()){
throw new Error(`Directory source is not supported for archive cache: ${localSource}`)
}
await fs.copy(localSource, destination, { overwrite: true })
return destination
}
async function extractZipToDirectory(zipPath, destination){
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'launcher-assets-'))
try {
await fs.ensureDir(tempDir)
new AdmZip(zipPath).extractAllTo(tempDir, true)
const rootEntries = (await fs.readdir(tempDir)).filter((entry) => entry !== '__MACOSX')
await fs.remove(destination)
if(rootEntries.length === 1){
const onlyEntry = path.join(tempDir, rootEntries[0])
const entryStat = await fs.stat(onlyEntry)
if(entryStat.isDirectory()){
await fs.copy(onlyEntry, destination, { overwrite: true })
return
}
}
await fs.copy(tempDir, destination, { overwrite: true })
} finally {
await fs.remove(tempDir)
}
}
async function ensureCachedArchive(profileId, source, fileName){
const cachePath = getProfileCacheFile(profileId, fileName)
await downloadSourceToFile(source, cachePath)
return cachePath
}
exports.getProfileBaseDirectory = getProfileBaseDirectory
exports.getServerBundleDirectory = getServerBundleDirectory
exports.prefetchProfileAssets = async function(profile){
if(profile.worldArchiveUrl){
await ensureCachedArchive(profile.id, profile.worldArchiveUrl, 'world.zip')
}
if(profile.serverBundleUrl){
await ensureCachedArchive(profile.id, profile.serverBundleUrl, 'server.zip')
}
const currentState = ConfigManager.getLibraryProfileAssetState(profile.id)
ConfigManager.setLibraryProfileAssetState(profile.id, {
...currentState,
prefetchedAt: new Date().toISOString()
})
ConfigManager.save()
}
exports.ensureWorldInstalled = async function(profile, serverId){
if(!profile.worldArchiveUrl || !profile.worldDirectoryName){
return null
}
const savesDirectory = path.join(ConfigManager.getInstanceDirectory(), serverId, 'saves')
const targetWorldDirectory = path.join(savesDirectory, profile.worldDirectoryName)
await fs.ensureDir(savesDirectory)
if(isRemoteSource(profile.worldArchiveUrl) || profile.worldArchiveUrl.endsWith('.zip') || profile.worldArchiveUrl.startsWith('file://')){
const cachePath = await ensureCachedArchive(profile.id, profile.worldArchiveUrl, 'world.zip')
await extractZipToDirectory(cachePath, targetWorldDirectory)
} else {
await fs.remove(targetWorldDirectory)
await fs.copy(resolveLocalSource(profile.worldArchiveUrl), targetWorldDirectory, { overwrite: true })
}
ConfigManager.setLibraryQuickPlayWorld(profile.id, profile.worldDirectoryName)
ConfigManager.setLibraryProfileAssetState(profile.id, {
worldInstalledAt: new Date().toISOString(),
worldInstalledServerId: serverId,
worldDirectoryName: profile.worldDirectoryName
})
ConfigManager.save()
return targetWorldDirectory
}
exports.ensureServerBundleInstalled = async function(profile){
if(!profile.serverBundleUrl){
return null
}
const targetDirectory = getServerBundleDirectory(profile)
if(isRemoteSource(profile.serverBundleUrl) || profile.serverBundleUrl.endsWith('.zip') || profile.serverBundleUrl.startsWith('file://')){
const cachePath = await ensureCachedArchive(profile.id, profile.serverBundleUrl, 'server.zip')
await extractZipToDirectory(cachePath, targetDirectory)
} else {
await fs.remove(targetDirectory)
await fs.copy(resolveLocalSource(profile.serverBundleUrl), targetDirectory, { overwrite: true })
}
ConfigManager.setLibraryProfileAssetState(profile.id, {
serverBundleInstalledAt: new Date().toISOString(),
serverBundleDirectory: targetDirectory
})
ConfigManager.save()
return targetDirectory
}
exports.prepareProfileForLaunch = async function(profile, serverId){
if(profile.kind === 'map'){
return exports.ensureWorldInstalled(profile, serverId)
}
return null
}