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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"packages/ariadne",
"packages/daedalus",
"packages/labrinth-derive",
"packages/modrinth-content-management",
"packages/modrinth-log",
"packages/modrinth-maxmind",
"packages/modrinth-util",
Expand All @@ -32,7 +33,6 @@ actix-ws = "0.3.0"
arc-swap = "1.7.1"
argon2 = { version = "0.5.3", features = ["std"] }
ariadne = { path = "packages/ariadne" }
async-compression = { version = "0.4.32", default-features = false }
async-minecraft-ping = { path = "packages/async-minecraft-ping" }
async-recursion = "1.1.1"
async-stripe = { version = "0.41.0", default-features = false, features = [
Expand Down Expand Up @@ -120,6 +120,7 @@ lettre = { version = "0.11.19", default-features = false, features = [
maxminddb = "0.26.0"
meilisearch-sdk = { version = "0.30.0", default-features = false }
modrinth-log = { path = "packages/modrinth-log" }
modrinth-content-management = { path = "packages/modrinth-content-management" }
modrinth-util = { path = "packages/modrinth-util" }
muralpay = { path = "packages/muralpay" }
murmur2 = "0.1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { process_listener } from '@/helpers/events'
import { kill, list as listInstances } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_instance, getServerLatency } from '@/helpers/worlds'
import { getServerAddress } from '@/store/install.js'
import { add_server_to_instance, getServerAddress, getServerLatency } from '@/helpers/worlds'

interface BrowseServerInstance {
name: string
Expand Down
51 changes: 51 additions & 0 deletions apps/app-frontend/src/helpers/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,35 @@ export async function update_project(instanceId: string, projectPath: string): P
// Returns a path to the new project file
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'

export interface ResolutionPreferences {
game_versions?: string[]
loaders?: string[]
}

export interface ResolveContentRequest {
project_id: string
version_id?: string | null
content_type: Labrinth.Content.v3.ContentType
selected?: ResolutionPreferences
}

export interface ResolvedContent {
project_id: string
version_id: string
dependent_on_version_id?: string | null
}

export interface ResolveContentPlan {
primary: ResolvedContent
dependencies: ResolvedContent[]
skipped: Array<{
project_id: string
version_id?: string | null
dependent_on_version_id?: string | null
reason: string
}>
}

export async function add_project_from_version(
instanceId: string,
versionId: string,
Expand All @@ -196,6 +225,28 @@ export async function add_project_from_version(
})
}

export async function install_project_with_dependencies(
instanceId: string,
request: ResolveContentRequest,
): Promise<ResolveContentPlan> {
return await invoke('plugin:instance|instance_install_project_with_dependencies', {
instanceId,
request,
})
}

export async function switch_project_version_with_dependencies(
instanceId: string,
projectPath: string,
versionId: string,
): Promise<string> {
return await invoke('plugin:instance|instance_switch_project_version_with_dependencies', {
instanceId,
projectPath,
versionId,
})
}

// Add a project to an instance from a path + project_type
// Returns a path to the new project file
export async function add_project_from_path(
Expand Down
22 changes: 22 additions & 0 deletions apps/app-frontend/src/helpers/worlds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,28 @@ export function resolveManagedServerWorld(
)
}

export function getServerAddress(javaServer?: { address?: string | null } | null) {
if (!javaServer) return null
return javaServer.address ?? null
}

export async function ensureManagedServerWorldExists(
instanceId: string,
serverName: string,
serverAddress: string | null,
) {
if (!instanceId || !serverAddress) return
try {
const worlds = await get_instance_worlds(instanceId)
const managedWorld = resolveManagedServerWorld(worlds, serverName, serverAddress)
if (!managedWorld) {
await add_server_to_instance(instanceId, serverName, serverAddress, 'prompt')
}
} catch (err) {
console.error('Failed to ensure managed server world exists:', err)
}
}

export async function getServerLatency(
address: string,
protocolVersion: ProtocolVersion | null = null,
Expand Down
94 changes: 46 additions & 48 deletions apps/app-frontend/src/pages/instance/Mods.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,20 @@ import {
} from '@/helpers/events.js'
import {
add_project_from_path,
add_project_from_version,
duplicate,
edit,
get,
get_linked_modpack_content,
list,
remove_project,
switch_project_version_with_dependencies,
toggle_disable_project,
update_all,
update_managed_modrinth_version,
update_project,
} from '@/helpers/instance'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
import { highlightModInInstance } from '@/helpers/utils.js'
import { injectContentInstall } from '@/providers/content-install'
import { installVersionDependencies } from '@/store/install'

const messages = defineMessages({
shareTitle: {
Expand Down Expand Up @@ -170,7 +167,8 @@ let savedModalState: ModpackContentModalState | null = null

const { formatMessage } = useVIntl()
const { handleError, addNotification } = injectNotificationManager()
const { installingItems } = injectContentInstall()
const { installingItems, installRevisionByInstance, installFailureRevisionByInstance } =
injectContentInstall()
const router = useRouter()
const queryClient = useQueryClient()
const debug = useDebugLogger('Mods:ContentUpdate')
Expand All @@ -186,6 +184,7 @@ const loading = ref(true)
const projects = ref<ContentItem[]>([])

const installingBuffer = ref<ContentItem[]>([])
const handledInstallRevision = ref(0)

watch(
() => installingItems.value.get(props.instance.id),
Expand All @@ -209,11 +208,25 @@ const mergedProjects = computed<ContentItem[]>(() => {
const active = installingItems.value.get(props.instance.id)
const pending = active ?? installingBuffer.value
if (pending.length === 0) return projects.value
const realProjectIds = new Set(projects.value.map((p) => p.project?.id).filter(Boolean))
const pendingProjectIds = new Set(pending.map((p) => p.project?.id).filter(Boolean))
const displayProjects = projects.value.map((project) =>
project.project?.id && pendingProjectIds.has(project.project.id)
? { ...project, installing: true }
: project,
)
const realProjectIds = new Set(displayProjects.map((p) => p.project?.id).filter(Boolean))
const placeholders = pending.filter((item) => !realProjectIds.has(item.project?.id))
return placeholders.length > 0 ? [...projects.value, ...placeholders] : projects.value
return placeholders.length > 0 ? [...displayProjects, ...placeholders] : displayProjects
})

watch(
() => installFailureRevisionByInstance.value.get(props.instance.id) ?? 0,
(revision, previousRevision) => {
if (revision === previousRevision) return
installingBuffer.value = []
},
)

const linkedModpackProject = ref<ContentModpackCardProject | null>(null)
const linkedModpackVersion = ref<ContentModpackCardVersion | null>(null)
const linkedModpackOwner = ref<ContentOwner | null>(null)
Expand Down Expand Up @@ -600,19 +613,11 @@ async function updateProject(mod: ContentItem) {

try {
const updateVersionId = mod.update_version_id!
await update_project(props.instance.id, mod.file_path)

if (updateVersionId) {
const versionData = await get_version(updateVersionId, 'must_revalidate').catch(handleError)

if (versionData) {
const instance = await get(props.instance.id).catch(handleError)

if (instance) {
await installVersionDependencies(instance, versionData, 'update').catch(handleError)
}
}
}
await switch_project_version_with_dependencies(
props.instance.id,
mod.file_path,
updateVersionId,
)

trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
Expand All @@ -636,27 +641,9 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
if (!operation) return

const oldPath = mod.file_path
const wasDisabled = mod.enabled === false || oldPath.endsWith('.disabled')
let newPath: string | null = null
let shouldRemoveNewOnError = false

try {
newPath = await add_project_from_version(props.instance.id, version.id, 'update')
shouldRemoveNewOnError = newPath !== oldPath

if (wasDisabled) {
newPath = await toggle_disable_project(props.instance.id, newPath)
}

const instance = await get(props.instance.id).catch(handleError)
if (instance) {
await installVersionDependencies(instance, version, 'update').catch(handleError)
}

shouldRemoveNewOnError = false
if (newPath !== oldPath) {
await remove_project(props.instance.id, oldPath)
}
await switch_project_version_with_dependencies(props.instance.id, oldPath, version.id)

trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
Expand All @@ -666,9 +653,6 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
project_type: mod.project_type,
})
} catch (err) {
if (shouldRemoveNewOnError && newPath && newPath !== oldPath) {
await remove_project(props.instance.id, newPath).catch(() => {})
}
handleError(err as Error)
} finally {
await refreshContentState('must_revalidate')
Expand Down Expand Up @@ -862,6 +846,15 @@ async function refreshContentState(cacheBehaviour?: CacheBehaviour) {
await refreshModpackContentItems(cacheBehaviour)
}

watch(
() => installRevisionByInstance.value.get(props.instance.id) ?? 0,
async (revision) => {
if (revision <= handledInstallRevision.value) return
handledInstallRevision.value = revision
await refreshContentState('must_revalidate')
},
)

async function handleModpackUpdate() {
if (!props.instance?.link?.project_id) return

Expand Down Expand Up @@ -1187,14 +1180,12 @@ provideContentManager({
project: linkedModpackProject.value,
projectLink: {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
query: { i: props.instance.id },
},
version: linkedModpackVersion.value ?? undefined,
versionLink:
linkedModpackProject.value && linkedModpackVersion.value
? {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`,
query: { i: props.instance.id },
}
: undefined,
owner: linkedModpackOwner.value
Expand Down Expand Up @@ -1257,9 +1248,7 @@ provideContentManager({
title: item.file_name.replace('.disabled', ''),
icon_url: null,
},
projectLink: item.project?.id
? { path: `/project/${item.project.id}`, query: { i: props.instance.id } }
: undefined,
projectLink: item.project?.id ? { path: `/project/${item.project.id}` } : undefined,
version: item.version ?? {
id: item.file_name,
version_number: formatMessage(commonMessages.unknownLabel),
Expand All @@ -1269,7 +1258,6 @@ provideContentManager({
item.project?.id && item.version?.id
? {
path: `/project/${item.project.id}/version/${item.version.id}`,
query: { i: props.instance.id },
}
: undefined,
owner: item.owner
Expand All @@ -1289,7 +1277,17 @@ type UnlistenFn = () => void
const initialContentReady = loadInitialContent()
void initialContentReady.then(restoreModpackContentModalState).catch(handleError)

function getInstallRevision() {
return installRevisionByInstance.value.get(props.instance.id) ?? 0
}

function loadInitialContent() {
const installRevision = getInstallRevision()
if (installRevision > handledInstallRevision.value) {
handledInstallRevision.value = installRevision
return initProjects('must_revalidate')
}

if (props.preloadedContent && applyContentData(props.preloadedContent)) {
return Promise.resolve()
}
Expand Down
2 changes: 1 addition & 1 deletion apps/app-frontend/src/pages/instance/Worlds.vue
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ import { get_project, get_project_v3 } from '@/helpers/cache.js'
import { instance_listener } from '@/helpers/events'
import { get_game_versions } from '@/helpers/tags'
import type { GameInstance } from '@/helpers/types'
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
import {
delete_world,
get_instance_protocol_version,
Expand Down Expand Up @@ -220,7 +221,6 @@ import {
} from '@/helpers/worlds.ts'
import { injectServerInstall } from '@/providers/server-install'
import { handleSevereError } from '@/store/error.js'
import { ensureManagedServerWorldExists, getServerAddress } from '@/store/install'

const messages = defineMessages({
removeServerTitle: {
Expand Down
Loading
Loading