fix: servers misc fixes (#5475)
* fix: tags in project settings to have icons and ordered correctly * fix copy in project list layout settings * fix tag item in header navigation * adjust ping ranges * add handle click tag * fix: dont show offline in project page for draft status * move tags above creators in app * preload server project page on load and optimize queries * add server project card to organization page * fix minecraft_java_server label * pnpm prepr * have user option in project create modal be circle * feat: implement better mobile project page view * disable summary line clamp for servers * fix: unlink instance doesnt update instance * increase icon upload size * small fix on button size * improve how server ping info loads * remove unnecessary pings for instance page * fix order of computing dependency diff * remove linked_project_id from world, use name+address to match for managed world instead * pnpm prepr * hide duplicate worlds with same domain name in worlds list * add install content warning for server instance * increase summary max width * add handling for server projects for bulk editing links * implement include user unlisted projects in published modpack select * pnpm prepr * filter to only user unlisted status * add bad link warnings * fix modpack tags appearing in server * cargo fmt
This commit is contained in:
@@ -49,6 +49,9 @@ const modalConfirmUnpair = ref()
|
||||
const modalConfirmReinstall = ref()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
const emit = defineEmits<{
|
||||
unlinked: []
|
||||
}>()
|
||||
|
||||
const loader = ref(props.instance.loader)
|
||||
const gameVersion = ref(props.instance.game_version)
|
||||
@@ -273,7 +276,7 @@ async function unpairProfile() {
|
||||
modpackProject.value = null
|
||||
modpackVersion.value = null
|
||||
modpackVersions.value = null
|
||||
modalConfirmUnpair.value.hide()
|
||||
emit('unlinked')
|
||||
}
|
||||
|
||||
async function repairModpack() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
@@ -49,16 +49,16 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['proceed'])
|
||||
const modal = ref(null)
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
hide_ads_window()
|
||||
modal.value.show()
|
||||
modal.value?.show()
|
||||
},
|
||||
hide: () => {
|
||||
onModalHide()
|
||||
modal.value.hide()
|
||||
modal.value?.hide()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -32,8 +32,12 @@ import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
const emit = defineEmits<{
|
||||
unlinked: []
|
||||
}>()
|
||||
|
||||
const isMinecraftServer = ref(false)
|
||||
const handleUnlinked = () => emit('unlinked')
|
||||
|
||||
watch(
|
||||
() => props.instance,
|
||||
@@ -121,7 +125,14 @@ defineExpose({ show })
|
||||
|
||||
<TabbedModal
|
||||
:tabs="
|
||||
tabs.map((tab) => ({ ...tab, props: { ...props, isMinecraftServer: isMinecraftServer } }))
|
||||
tabs.map((tab) => ({
|
||||
...tab,
|
||||
props: {
|
||||
...props,
|
||||
isMinecraftServer,
|
||||
onUnlinked: handleUnlinked,
|
||||
},
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</ModalWrapper>
|
||||
|
||||
@@ -187,10 +187,11 @@ type ProjectInfo = {
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const installStore = useInstall()
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const instance = ref<GameInstance | null>(null)
|
||||
const onUpdateComplete = ref<() => void>(() => {})
|
||||
const onUpdateComplete = ref<UpdateCompleteCallback>(() => {})
|
||||
const diffs = ref<DependencyDiff[]>([])
|
||||
const modpackVersionId = ref<string | null>(null)
|
||||
const modpackVersion = ref<Version | null>(null)
|
||||
@@ -316,6 +317,7 @@ async function computeDependencyDiffs(
|
||||
|
||||
async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[] | null> {
|
||||
if (!inst.linked_data) return null
|
||||
if (!modpackVersionId.value || !inst.linked_data.version_id) return null
|
||||
|
||||
try {
|
||||
// For server projects, linked_data.project_id is the server project but
|
||||
@@ -327,8 +329,8 @@ async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[
|
||||
// Compute dependency diffs between current and latest version
|
||||
if (instanceModpackVersion && modpackVersion.value) {
|
||||
return await computeDependencyDiffs(
|
||||
modpackVersion.value.dependencies || [],
|
||||
instanceModpackVersion.dependencies || [],
|
||||
modpackVersion.value.dependencies || [],
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -355,7 +357,7 @@ async function handleUpdate() {
|
||||
try {
|
||||
if (modpackVersionId.value && instance.value) {
|
||||
await update_managed_modrinth_version(instance.value.path, modpackVersionId.value)
|
||||
onUpdateComplete.value()
|
||||
await onUpdateComplete.value()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating instance:', error)
|
||||
@@ -379,7 +381,7 @@ function handleDecline() {
|
||||
function show(
|
||||
instanceVal: GameInstance,
|
||||
modpackVersionIdVal: string | null = null,
|
||||
callback: () => void = () => {},
|
||||
callback: UpdateCompleteCallback = () => {},
|
||||
e?: MouseEvent,
|
||||
) {
|
||||
instance.value = instanceVal
|
||||
|
||||
@@ -45,7 +45,7 @@ import type {
|
||||
SingleplayerWorld,
|
||||
World,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { getWorldIdentifier, isLinkedWorld, set_world_display_status } from '@/helpers/worlds.ts'
|
||||
import { getWorldIdentifier, set_world_display_status } from '@/helpers/worlds.ts'
|
||||
|
||||
import { LockIcon } from '../../../../../../packages/assets/generated-icons'
|
||||
|
||||
@@ -81,6 +81,8 @@ const props = withDefaults(
|
||||
message: MessageDescriptor
|
||||
}
|
||||
|
||||
managed?: boolean
|
||||
|
||||
// Instance
|
||||
instancePath?: string
|
||||
instanceName?: string
|
||||
@@ -99,6 +101,7 @@ const props = withDefaults(
|
||||
renderedMotd: undefined,
|
||||
|
||||
gameMode: undefined,
|
||||
managed: false,
|
||||
|
||||
instancePath: undefined,
|
||||
instanceName: undefined,
|
||||
@@ -120,7 +123,7 @@ const serverIncompatible = computed(
|
||||
)
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
const linked = computed(() => isLinkedWorld(props.world))
|
||||
const managed = computed(() => props.managed)
|
||||
|
||||
const messages = defineMessages({
|
||||
hardcore: {
|
||||
@@ -209,7 +212,7 @@ const messages = defineMessages({
|
||||
{{ world.name }}
|
||||
</div>
|
||||
<TagItem
|
||||
v-if="linked"
|
||||
v-if="managed"
|
||||
v-tooltip="formatMessage(messages.linkedServer)"
|
||||
class="border !border-solid border-blue bg-highlight-blue text-xs"
|
||||
:style="`--_color: var(--color-blue)`"
|
||||
@@ -412,10 +415,10 @@ const messages = defineMessages({
|
||||
id: 'edit',
|
||||
action: () => emit('edit'),
|
||||
shown: !instancePath,
|
||||
disabled: locked || linked,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: linked
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
@@ -452,10 +455,10 @@ const messages = defineMessages({
|
||||
hoverFilled: true,
|
||||
action: () => emit('delete'),
|
||||
shown: !instancePath,
|
||||
disabled: locked || linked,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: linked
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
|
||||
@@ -30,7 +30,6 @@ export type ServerWorld = BaseWorld & {
|
||||
index: number
|
||||
address: string
|
||||
pack_status: ServerPackStatus
|
||||
linked_project_id?: string
|
||||
}
|
||||
|
||||
export type World = SingleplayerWorld | ServerWorld
|
||||
@@ -141,14 +140,12 @@ export async function add_server_to_profile(
|
||||
name: string,
|
||||
address: string,
|
||||
packStatus: ServerPackStatus,
|
||||
linkedProjectId?: string,
|
||||
): Promise<number> {
|
||||
return await invoke('plugin:worlds|add_server_to_profile', {
|
||||
path,
|
||||
name,
|
||||
address,
|
||||
packStatus,
|
||||
linkedProjectId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,7 +155,6 @@ export async function edit_server_in_profile(
|
||||
name: string,
|
||||
address: string,
|
||||
packStatus: ServerPackStatus,
|
||||
linkedProjectId?: string,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:worlds|edit_server_in_profile', {
|
||||
path,
|
||||
@@ -166,7 +162,6 @@ export async function edit_server_in_profile(
|
||||
name,
|
||||
address,
|
||||
packStatus,
|
||||
linkedProjectId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,11 +199,6 @@ export function getWorldIdentifier(world: World) {
|
||||
|
||||
export function sortWorlds(worlds: World[]) {
|
||||
worlds.sort((a, b) => {
|
||||
const aLinked = isLinkedWorld(a)
|
||||
const bLinked = isLinkedWorld(b)
|
||||
if (aLinked !== bLinked) {
|
||||
return aLinked ? -1 : 1
|
||||
}
|
||||
if (!a.last_played) {
|
||||
return 1
|
||||
}
|
||||
@@ -227,8 +217,129 @@ export function isServerWorld(world: World): world is ServerWorld {
|
||||
return world.type === 'server'
|
||||
}
|
||||
|
||||
export function isLinkedWorld(world: World): boolean {
|
||||
return world.type === 'server' && !!world.linked_project_id
|
||||
const DEFAULT_MINECRAFT_SERVER_PORT = 25565
|
||||
|
||||
function parseServerPort(port: string): number | null {
|
||||
const parsed = Number.parseInt(port, 10)
|
||||
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null
|
||||
}
|
||||
|
||||
function parseServerHost(address: string): string {
|
||||
const trimmedAddress = address.trim()
|
||||
if (!trimmedAddress) return ''
|
||||
|
||||
if (trimmedAddress.startsWith('[')) {
|
||||
const closingBracket = trimmedAddress.indexOf(']')
|
||||
if (closingBracket > 0) {
|
||||
return trimmedAddress.slice(1, closingBracket).trim().toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
const firstColon = trimmedAddress.indexOf(':')
|
||||
const lastColon = trimmedAddress.lastIndexOf(':')
|
||||
|
||||
if (firstColon !== -1 && firstColon === lastColon) {
|
||||
return trimmedAddress.slice(0, firstColon).trim().toLowerCase()
|
||||
}
|
||||
|
||||
return trimmedAddress.toLowerCase()
|
||||
}
|
||||
|
||||
function isIPv4Host(host: string): boolean {
|
||||
const segments = host.split('.')
|
||||
if (segments.length !== 4) return false
|
||||
|
||||
return segments.every((segment) => {
|
||||
if (!/^\d+$/.test(segment)) return false
|
||||
const value = Number.parseInt(segment, 10)
|
||||
return value >= 0 && value <= 255
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalization converts addresses to a canonical form (lowercase-host:port, default port 25565)
|
||||
*/
|
||||
export function normalizeServerAddress(address: string): string {
|
||||
const trimmedAddress = address.trim()
|
||||
const host = parseServerHost(trimmedAddress)
|
||||
if (!host) return ''
|
||||
let port = DEFAULT_MINECRAFT_SERVER_PORT
|
||||
|
||||
// ipv6 address
|
||||
if (trimmedAddress.startsWith('[')) {
|
||||
const closingBracket = trimmedAddress.indexOf(']')
|
||||
if (closingBracket > 0) {
|
||||
const suffix = trimmedAddress.slice(closingBracket + 1)
|
||||
if (suffix.startsWith(':')) {
|
||||
const parsedPort = parseServerPort(suffix.slice(1))
|
||||
if (parsedPort != null) {
|
||||
port = parsedPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ipv4 address or hostname
|
||||
} else {
|
||||
const firstColon = trimmedAddress.indexOf(':')
|
||||
const lastColon = trimmedAddress.lastIndexOf(':')
|
||||
if (firstColon !== -1 && firstColon === lastColon) {
|
||||
const parsedPort = parseServerPort(trimmedAddress.slice(firstColon + 1))
|
||||
if (parsedPort != null) {
|
||||
port = parsedPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${host}:${port}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain key used for deduping server entries by removing a single leading subdomain.
|
||||
* Example: test.cobblemon.gg and cobblemon.gg map to cobblemon.gg
|
||||
*/
|
||||
export function getServerDomainKey(address: string): string {
|
||||
const normalizedAddress = normalizeServerAddress(address)
|
||||
if (!normalizedAddress) return ''
|
||||
|
||||
const separator = normalizedAddress.lastIndexOf(':')
|
||||
if (separator <= 0 || separator === normalizedAddress.length - 1) return normalizedAddress
|
||||
|
||||
const host = normalizedAddress.slice(0, separator).replace(/\.+$/, '')
|
||||
if (!host) return normalizedAddress
|
||||
if (host.includes(':') || isIPv4Host(host)) return normalizedAddress
|
||||
|
||||
const segments = host.split('.').filter(Boolean)
|
||||
if (segments.length <= 2) return host
|
||||
|
||||
return segments.slice(1).join('.')
|
||||
}
|
||||
|
||||
export function resolveManagedServerWorld(
|
||||
worlds: World[],
|
||||
managedName: string | null | undefined,
|
||||
managedAddress: string | null | undefined,
|
||||
): ServerWorld | null {
|
||||
if (!managedName || !managedAddress) return null
|
||||
|
||||
const normalizedManagedAddress = normalizeServerAddress(managedAddress)
|
||||
if (!normalizedManagedAddress) return null
|
||||
|
||||
const servers = worlds
|
||||
.filter(isServerWorld)
|
||||
.slice()
|
||||
.sort((a, b) => a.index - b.index)
|
||||
|
||||
const exactMatch = servers.find(
|
||||
(server) =>
|
||||
server.name === managedName &&
|
||||
normalizeServerAddress(server.address) === normalizedManagedAddress,
|
||||
)
|
||||
if (exactMatch) return exactMatch
|
||||
|
||||
return (
|
||||
servers.find((server) => normalizeServerAddress(server.address) === normalizedManagedAddress) ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerLatency(
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import type { ProjectType, SortType, Tags } from '@modrinth/ui'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
@@ -724,6 +725,10 @@ previousFilterState.value = JSON.stringify({
|
||||
<template v-if="instance">
|
||||
<InstanceIndicator :instance="instance" />
|
||||
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
||||
<Admonition v-if="isServerInstance" type="warning" class="mb-1">
|
||||
Adding content can break compatibility when joining the server. Any added content will also
|
||||
be lost when you update the server instance content.
|
||||
</Admonition>
|
||||
</template>
|
||||
<NavTabs :links="selectableProjectTypes" />
|
||||
<StyledInput
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
<div v-if="instance">
|
||||
<div class="p-6 pr-2 pb-4" @contextmenu.prevent.stop="(event) => handleRightClick(event)">
|
||||
<ExportModal ref="exportModal" :instance="instance" />
|
||||
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
|
||||
<InstanceSettingsModal
|
||||
ref="settingsModal"
|
||||
:instance="instance"
|
||||
:offline="offline"
|
||||
@unlinked="fetchInstance"
|
||||
/>
|
||||
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
@@ -55,27 +60,26 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<ServerOnlinePlayers
|
||||
v-if="playersOnline !== undefined"
|
||||
:online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
hide-label
|
||||
/>
|
||||
|
||||
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
||||
|
||||
<div
|
||||
v-if="
|
||||
(playersOnline !== undefined || recentPlays !== undefined) &&
|
||||
(minecraftServer?.region || ping)
|
||||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
<template v-if="loadingServerPing">
|
||||
<ServerOnlinePlayers
|
||||
v-if="playersOnline !== undefined"
|
||||
:online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
hide-label
|
||||
/>
|
||||
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
||||
<div
|
||||
v-if="
|
||||
(playersOnline !== undefined || recentPlays !== undefined) &&
|
||||
(minecraftServer?.region || ping)
|
||||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
<ServerPing v-if="ping" :ping="ping" />
|
||||
</template>
|
||||
|
||||
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
|
||||
|
||||
<ServerPing v-if="ping" :ping="ping" />
|
||||
|
||||
<div
|
||||
v-if="minecraftServer?.region || ping"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
@@ -329,7 +333,7 @@ import { get_by_profile_path } from '@/helpers/process'
|
||||
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { get_server_status, getServerLatency } from '@/helpers/worlds'
|
||||
import { get_server_status } from '@/helpers/worlds'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { playServerProject } from '@/store/install.js'
|
||||
import { useBreadcrumbs, useLoading } from '@/store/state'
|
||||
@@ -370,6 +374,7 @@ const recentPlays = computed(
|
||||
)
|
||||
const playersOnline = ref<number | undefined>(undefined)
|
||||
const ping = ref<number | undefined>(undefined)
|
||||
const loadingServerPing = ref(false)
|
||||
|
||||
async function fetchInstance() {
|
||||
isServerInstance.value = false
|
||||
@@ -377,6 +382,7 @@ async function fetchInstance() {
|
||||
modrinthVersions.value = []
|
||||
ping.value = undefined
|
||||
playersOnline.value = undefined
|
||||
loadingServerPing.value = false
|
||||
|
||||
instance.value = await get(route.params.id as string).catch(handleError)
|
||||
|
||||
@@ -409,14 +415,19 @@ async function fetchInstance() {
|
||||
function fetchDeferredData() {
|
||||
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
|
||||
if (isServerInstance.value && serverAddress) {
|
||||
Promise.all([get_server_status(serverAddress), getServerLatency(serverAddress)])
|
||||
.then(([status, latency]) => {
|
||||
ping.value = latency
|
||||
get_server_status(serverAddress)
|
||||
.then((status) => {
|
||||
playersOnline.value = status.players?.online
|
||||
ping.value = status.ping
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Failed to ping server ${serverAddress}:`, err)
|
||||
.catch((error) => {
|
||||
console.error(`Failed to fetch server status for ${serverAddress}:`, error)
|
||||
})
|
||||
.finally(() => {
|
||||
loadingServerPing.value = true
|
||||
})
|
||||
} else {
|
||||
loadingServerPing.value = true
|
||||
}
|
||||
|
||||
updatePlayState()
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
:description="`'${worldToDelete?.name}' will be **permanently deleted**, and there will be no way to recover it.`"
|
||||
@proceed="proceedDeleteWorld"
|
||||
/>
|
||||
<div v-if="worlds.length > 0" class="flex flex-col gap-4">
|
||||
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
@@ -62,6 +62,7 @@
|
||||
v-for="world in filteredWorlds"
|
||||
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
||||
:world="world"
|
||||
:managed="world.type === 'server' ? isManagedServerWorld(world) : false"
|
||||
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
||||
:supports-server-quick-play="supportsServerQuickPlay"
|
||||
:supports-world-quick-play="supportsWorldQuickPlay"
|
||||
@@ -80,13 +81,13 @@
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
@edit="
|
||||
() =>
|
||||
isLinkedWorld(world)
|
||||
? undefined
|
||||
: world.type === 'server'
|
||||
? editServerModal?.show(world)
|
||||
: editWorldModal?.show(world)
|
||||
world.type === 'singleplayer'
|
||||
? editWorldModal?.show(world)
|
||||
: isManagedServerWorld(world)
|
||||
? undefined
|
||||
: editServerModal?.show(world)
|
||||
"
|
||||
@delete="() => !isLinkedWorld(world) && promptToRemoveWorld(world)"
|
||||
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
@@ -144,17 +145,19 @@ import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import { get_project, get_project_v3 } from '@/helpers/cache.js'
|
||||
import { profile_listener } from '@/helpers/events'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import {
|
||||
delete_world,
|
||||
get_profile_protocol_version,
|
||||
getServerDomainKey,
|
||||
getWorldIdentifier,
|
||||
handleDefaultProfileUpdateEvent,
|
||||
hasServerQuickPlaySupport,
|
||||
hasWorldQuickPlaySupport,
|
||||
isLinkedWorld,
|
||||
normalizeServerAddress,
|
||||
type ProfileEvent,
|
||||
type ProtocolVersion,
|
||||
refreshServerData,
|
||||
@@ -162,6 +165,7 @@ import {
|
||||
refreshWorld,
|
||||
refreshWorlds,
|
||||
remove_server_from_profile,
|
||||
resolveManagedServerWorld,
|
||||
type ServerData,
|
||||
type ServerWorld,
|
||||
showWorldInFolder,
|
||||
@@ -171,7 +175,11 @@ import {
|
||||
start_join_singleplayer_world,
|
||||
type World,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { playServerProject } from '@/store/install'
|
||||
import {
|
||||
ensureManagedServerWorldExists,
|
||||
getServerAddress,
|
||||
playServerProject,
|
||||
} from '@/store/install'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const route = useRoute()
|
||||
@@ -225,6 +233,69 @@ const linuxRefreshCount = ref(0)
|
||||
const protocolVersion = ref<ProtocolVersion | null>(
|
||||
await get_profile_protocol_version(instance.value.path),
|
||||
)
|
||||
const managedServerName = ref<string | null>(null)
|
||||
const managedServerAddress = ref<string | null>(null)
|
||||
|
||||
const managedServerWorld = computed(() =>
|
||||
resolveManagedServerWorld(worlds.value, managedServerName.value, managedServerAddress.value),
|
||||
)
|
||||
|
||||
function isManagedServerWorld(world: World): world is ServerWorld {
|
||||
return world.type === 'server' && managedServerWorld.value?.index === world.index
|
||||
}
|
||||
|
||||
async function refreshManagedServerMetadata() {
|
||||
await ensureManagedServerWorldExists(
|
||||
instance.value.path,
|
||||
managedServerName.value,
|
||||
managedServerAddress.value,
|
||||
)
|
||||
|
||||
const projectId = instance.value.linked_data?.project_id
|
||||
if (!projectId) {
|
||||
managedServerName.value = null
|
||||
managedServerAddress.value = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const [project, projectV3] = await Promise.all([
|
||||
get_project(projectId, 'bypass'),
|
||||
get_project_v3(projectId, 'bypass'),
|
||||
])
|
||||
|
||||
if (projectV3?.minecraft_server == null) {
|
||||
managedServerName.value = null
|
||||
managedServerAddress.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const serverAddress = getServerAddress(projectV3.minecraft_java_server)
|
||||
if (!serverAddress) {
|
||||
managedServerName.value = null
|
||||
managedServerAddress.value = null
|
||||
return
|
||||
}
|
||||
|
||||
managedServerName.value = project.title
|
||||
managedServerAddress.value = serverAddress
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to resolve managed server metadata for profile: ${instance.value.path}`,
|
||||
err,
|
||||
)
|
||||
managedServerName.value = null
|
||||
managedServerAddress.value = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => instance.value.linked_data?.project_id,
|
||||
async () => {
|
||||
await refreshManagedServerMetadata()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
@@ -257,6 +328,7 @@ async function refreshAllWorlds() {
|
||||
console.log(`Already refreshing, cancelling refresh.`)
|
||||
return
|
||||
}
|
||||
await refreshManagedServerMetadata()
|
||||
|
||||
refreshingAll.value = true
|
||||
|
||||
@@ -334,8 +406,11 @@ async function joinWorld(world: World) {
|
||||
startingInstance.value = true
|
||||
worldPlaying.value = world
|
||||
if (world.type === 'server') {
|
||||
if (isLinkedWorld(world)) {
|
||||
playServerProject(world.linked_project_id)
|
||||
const managedProjectId = instance.value.linked_data?.project_id
|
||||
if (managedProjectId && isManagedServerWorld(world)) {
|
||||
await playServerProject(managedProjectId).catch(handleJoinError)
|
||||
startingInstance.value = false
|
||||
return
|
||||
}
|
||||
await start_join_server(instance.value.path, world.address).catch(handleJoinError)
|
||||
} else if (world.type === 'singleplayer') {
|
||||
@@ -379,12 +454,48 @@ const supportsWorldQuickPlay = computed(() =>
|
||||
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
|
||||
const dedupedWorlds = computed(() => {
|
||||
const visibleWorlds: World[] = []
|
||||
const serverIndexByDomain = new Map<string, number>()
|
||||
|
||||
for (const world of worlds.value) {
|
||||
if (world.type !== 'server') {
|
||||
visibleWorlds.push(world)
|
||||
continue
|
||||
}
|
||||
|
||||
const domainKey =
|
||||
getServerDomainKey(world.address) ||
|
||||
normalizeServerAddress(world.address) ||
|
||||
`server-${world.index}`
|
||||
const existingIndex = serverIndexByDomain.get(domainKey)
|
||||
|
||||
if (existingIndex == null) {
|
||||
serverIndexByDomain.set(domainKey, visibleWorlds.length)
|
||||
visibleWorlds.push(world)
|
||||
continue
|
||||
}
|
||||
|
||||
// replace world with managed world if applicable
|
||||
const existingWorld = visibleWorlds[existingIndex]
|
||||
if (
|
||||
existingWorld?.type === 'server' &&
|
||||
!isManagedServerWorld(existingWorld) &&
|
||||
isManagedServerWorld(world)
|
||||
) {
|
||||
visibleWorlds[existingIndex] = world
|
||||
}
|
||||
}
|
||||
|
||||
return visibleWorlds
|
||||
})
|
||||
|
||||
const filterOptions = computed(() => {
|
||||
const options: FilterBarOption[] = []
|
||||
|
||||
const hasServer = worlds.value.some((x) => x.type === 'server')
|
||||
const hasServer = dedupedWorlds.value.some((x) => x.type === 'server')
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'singleplayer') && hasServer) {
|
||||
if (dedupedWorlds.value.some((x) => x.type === 'singleplayer') && hasServer) {
|
||||
options.push({
|
||||
id: 'singleplayer',
|
||||
message: messages.singleplayer,
|
||||
@@ -398,13 +509,13 @@ const filterOptions = computed(() => {
|
||||
if (hasServer) {
|
||||
// add available filter if there's any offline ("unavailable") servers AND there's any singleplayer worlds or available servers
|
||||
if (
|
||||
worlds.value.some(
|
||||
dedupedWorlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'server' &&
|
||||
!serverData.value[x.address]?.status &&
|
||||
!serverData.value[x.address]?.refreshing,
|
||||
) &&
|
||||
worlds.value.some(
|
||||
dedupedWorlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'singleplayer' ||
|
||||
(x.type === 'server' &&
|
||||
@@ -423,7 +534,7 @@ const filterOptions = computed(() => {
|
||||
})
|
||||
|
||||
const filteredWorlds = computed(() =>
|
||||
worlds.value.filter((x) => {
|
||||
dedupedWorlds.value.filter((x) => {
|
||||
const availableFilter = filters.value.includes('available')
|
||||
const typeFilter = filters.value.includes('server') || filters.value.includes('singleplayer')
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
:project-v3="projectV3"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarTags :project="data" class="project-sidebar-section" />
|
||||
<ProjectSidebarCreators
|
||||
:organization="null"
|
||||
:members="members"
|
||||
@@ -34,7 +35,6 @@
|
||||
link-target="_blank"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarTags :project="data" class="project-sidebar-section" />
|
||||
<ProjectSidebarDetails
|
||||
:project="data"
|
||||
:has-versions="versions.length > 0"
|
||||
@@ -79,7 +79,7 @@
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<ButtonStyled size="large" circular>
|
||||
<button v-tooltip="'Add server to instance'" @click="handleAddServerToInstance">
|
||||
<PlusIcon />
|
||||
</button>
|
||||
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
} from '@/helpers/profile.js'
|
||||
import {
|
||||
add_server_to_profile,
|
||||
edit_server_in_profile,
|
||||
get_profile_worlds,
|
||||
resolveManagedServerWorld,
|
||||
start_join_server,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import router from '@/routes.js'
|
||||
@@ -328,7 +328,7 @@ export const installServerProject = async (serverProjectId) => {
|
||||
})
|
||||
await edit_icon(profilePath, originalIconPath)
|
||||
|
||||
await syncServerProjectAsWorld(profilePath, project.title, serverAddress, serverProjectId)
|
||||
await ensureManagedServerWorldExists(profilePath, project.title, serverAddress)
|
||||
}
|
||||
|
||||
export const getServerAddress = (javaServer) => {
|
||||
@@ -337,61 +337,16 @@ export const getServerAddress = (javaServer) => {
|
||||
return address
|
||||
}
|
||||
|
||||
const syncServerProjectAsWorld = async (
|
||||
profilePath,
|
||||
serverName,
|
||||
serverAddress,
|
||||
serverProjectId = null,
|
||||
) => {
|
||||
export const ensureManagedServerWorldExists = async (profilePath, serverName, serverAddress) => {
|
||||
if (!profilePath || !serverAddress) return
|
||||
try {
|
||||
const worlds = await get_profile_worlds(profilePath)
|
||||
|
||||
if (serverProjectId) {
|
||||
// Check if a linked world for this project already exists
|
||||
const linkedWorld = worlds.find(
|
||||
(w) => w.type === 'server' && w.linked_project_id === serverProjectId,
|
||||
)
|
||||
if (linkedWorld) {
|
||||
// Sync linked world data with project details
|
||||
if (linkedWorld.address !== serverAddress || linkedWorld.name !== serverName) {
|
||||
await edit_server_in_profile(
|
||||
profilePath,
|
||||
linkedWorld.index,
|
||||
serverName,
|
||||
serverAddress,
|
||||
linkedWorld.pack_status,
|
||||
serverProjectId,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const existingServer = worlds.find((w) => w.type === 'server' && w.address === serverAddress)
|
||||
if (existingServer) {
|
||||
// Re-link and sync existing server (link may have been lost by Minecraft rewriting servers.dat)
|
||||
if (serverProjectId || existingServer.name !== serverName) {
|
||||
await edit_server_in_profile(
|
||||
profilePath,
|
||||
existingServer.index,
|
||||
serverName,
|
||||
serverAddress,
|
||||
existingServer.pack_status,
|
||||
serverProjectId ?? undefined,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
await add_server_to_profile(
|
||||
profilePath,
|
||||
serverName,
|
||||
serverAddress,
|
||||
'prompt',
|
||||
serverProjectId ?? undefined,
|
||||
)
|
||||
const managedWorld = resolveManagedServerWorld(worlds, serverName, serverAddress)
|
||||
if (!managedWorld) {
|
||||
await add_server_to_profile(profilePath, serverName, serverAddress, 'prompt')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add server to instance worlds:', err)
|
||||
console.error('Failed to ensure managed server world exists:', err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,8 +375,7 @@ const createVanillaServerInstance = async (project, gameVersion, serverAddress)
|
||||
},
|
||||
)
|
||||
|
||||
//
|
||||
await syncServerProjectAsWorld(profilePath, project.title, serverAddress, project.id)
|
||||
await ensureManagedServerWorldExists(profilePath, project.title, serverAddress)
|
||||
|
||||
return profilePath
|
||||
}
|
||||
@@ -552,7 +506,7 @@ export const playServerProject = async (projectId) => {
|
||||
|
||||
if (!instance) return
|
||||
|
||||
await syncServerProjectAsWorld(instance.path, project.title, serverAddress, project.id)
|
||||
await ensureManagedServerWorldExists(instance.path, project.title, serverAddress)
|
||||
|
||||
// Update existing instance if needed
|
||||
if (isModpack && instance.linked_data?.version_id !== modpackVersionId) {
|
||||
|
||||
Reference in New Issue
Block a user