Merge branch 'main' into cal/dependency-injection
Signed-off-by: IMB11 <hendersoncal117@gmail.com>
This commit is contained in:
commit
68a6f091fa
1
.idea/code.iml
generated
1
.idea/code.iml
generated
@ -10,7 +10,6 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/rust-common/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
"test": "vue-tsc --noEmit"
|
"test": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -21,14 +21,11 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const featuredCategory = computed(() => {
|
const featuredCategory = computed(() => {
|
||||||
if (props.project.categories.includes('optimization')) {
|
if (props.project.display_categories.includes('optimization')) {
|
||||||
return 'optimization'
|
return 'optimization'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.project.categories.length > 0) {
|
return props.project.display_categories[0] ?? props.project.categories[0]
|
||||||
return props.project.categories[0]
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const toColor = computed(() => {
|
const toColor = computed(() => {
|
||||||
|
|||||||
@ -6,9 +6,8 @@ import type {
|
|||||||
ServerWorld,
|
ServerWorld,
|
||||||
SingleplayerWorld,
|
SingleplayerWorld,
|
||||||
World,
|
World,
|
||||||
set_world_display_status,
|
|
||||||
getWorldIdentifier,
|
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
|
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
@ -61,7 +60,8 @@ const props = withDefaults(
|
|||||||
playingInstance?: boolean
|
playingInstance?: boolean
|
||||||
playingWorld?: boolean
|
playingWorld?: boolean
|
||||||
startingInstance?: boolean
|
startingInstance?: boolean
|
||||||
supportsQuickPlay?: boolean
|
supportsServerQuickPlay?: boolean
|
||||||
|
supportsWorldQuickPlay?: boolean
|
||||||
currentProtocol?: ProtocolVersion | null
|
currentProtocol?: ProtocolVersion | null
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
|
|
||||||
@ -85,7 +85,8 @@ const props = withDefaults(
|
|||||||
playingInstance: false,
|
playingInstance: false,
|
||||||
playingWorld: false,
|
playingWorld: false,
|
||||||
startingInstance: false,
|
startingInstance: false,
|
||||||
supportsQuickPlay: false,
|
supportsServerQuickPlay: true,
|
||||||
|
supportsWorldQuickPlay: false,
|
||||||
currentProtocol: null,
|
currentProtocol: null,
|
||||||
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
@ -128,9 +129,13 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.a_minecraft_server',
|
id: 'instance.worlds.a_minecraft_server',
|
||||||
defaultMessage: 'A Minecraft Server',
|
defaultMessage: 'A Minecraft Server',
|
||||||
},
|
},
|
||||||
noQuickPlay: {
|
noServerQuickPlay: {
|
||||||
id: 'instance.worlds.no_quick_play',
|
id: 'instance.worlds.no_server_quick_play',
|
||||||
defaultMessage: 'You can only jump straight into worlds on Minecraft 1.20+',
|
defaultMessage: 'You can only jump straight into servers on Minecraft Alpha 1.0.5+',
|
||||||
|
},
|
||||||
|
noSingleplayerQuickPlay: {
|
||||||
|
id: 'instance.worlds.no_singleplayer_quick_play',
|
||||||
|
defaultMessage: 'You can only jump straight into singleplayer worlds on Minecraft 1.20+',
|
||||||
},
|
},
|
||||||
gameAlreadyOpen: {
|
gameAlreadyOpen: {
|
||||||
id: 'instance.worlds.game_already_open',
|
id: 'instance.worlds.game_already_open',
|
||||||
@ -152,10 +157,6 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.view_instance',
|
id: 'instance.worlds.view_instance',
|
||||||
defaultMessage: 'View instance',
|
defaultMessage: 'View instance',
|
||||||
},
|
},
|
||||||
playAnyway: {
|
|
||||||
id: 'instance.worlds.play_anyway',
|
|
||||||
defaultMessage: 'Play anyway',
|
|
||||||
},
|
|
||||||
playInstance: {
|
playInstance: {
|
||||||
id: 'instance.worlds.play_instance',
|
id: 'instance.worlds.play_instance',
|
||||||
defaultMessage: 'Play instance',
|
defaultMessage: 'Play instance',
|
||||||
@ -330,17 +331,24 @@ const messages = defineMessages({
|
|||||||
<ButtonStyled v-else>
|
<ButtonStyled v-else>
|
||||||
<button
|
<button
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
!serverStatus
|
world.type == 'server' && !supportsServerQuickPlay
|
||||||
|
? formatMessage(messages.noServerQuickPlay)
|
||||||
|
: world.type == 'singleplayer' && !supportsWorldQuickPlay
|
||||||
|
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||||
|
: playingOtherWorld || locked
|
||||||
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
|
: !serverStatus
|
||||||
? formatMessage(messages.noContact)
|
? formatMessage(messages.noContact)
|
||||||
: serverIncompatible
|
: serverIncompatible
|
||||||
? formatMessage(messages.incompatibleServer)
|
? formatMessage(messages.incompatibleServer)
|
||||||
: !supportsQuickPlay
|
|
||||||
? formatMessage(messages.noQuickPlay)
|
|
||||||
: playingOtherWorld || locked
|
|
||||||
? formatMessage(messages.gameAlreadyOpen)
|
|
||||||
: null
|
: null
|
||||||
"
|
"
|
||||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
:disabled="
|
||||||
|
playingOtherWorld ||
|
||||||
|
startingInstance ||
|
||||||
|
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||||
|
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||||
|
"
|
||||||
@click="emit('play')"
|
@click="emit('play')"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||||
@ -357,11 +365,6 @@ const messages = defineMessages({
|
|||||||
disabled: playingInstance,
|
disabled: playingInstance,
|
||||||
action: () => emit('play-instance'),
|
action: () => emit('play-instance'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'play-anyway',
|
|
||||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
|
||||||
action: () => emit('play'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'open-instance',
|
id: 'open-instance',
|
||||||
shown: !!instancePath,
|
shown: !!instancePath,
|
||||||
@ -427,10 +430,6 @@ const messages = defineMessages({
|
|||||||
<PlayIcon aria-hidden="true" />
|
<PlayIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.playInstance) }}
|
{{ formatMessage(messages.playInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #play-anyway>
|
|
||||||
<PlayIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(messages.playAnyway) }}
|
|
||||||
</template>
|
|
||||||
<template #open-instance>
|
<template #open-instance>
|
||||||
<EyeIcon aria-hidden="true" />
|
<EyeIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.viewInstance) }}
|
{{ formatMessage(messages.viewInstance) }}
|
||||||
|
|||||||
@ -311,15 +311,24 @@ export async function refreshWorlds(instancePath: string): Promise<World[]> {
|
|||||||
return worlds ?? []
|
return worlds ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIRST_QUICK_PLAY_VERSION = '23w14a'
|
export function hasServerQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
||||||
|
if (!gameVersions.length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export function hasQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
||||||
|
const targetIndex = gameVersions.findIndex((v) => v.version === 'a1.0.5_01')
|
||||||
|
|
||||||
|
return versionIndex === -1 || targetIndex === -1 || versionIndex <= targetIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
||||||
if (!gameVersions.length) {
|
if (!gameVersions.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
||||||
const targetIndex = gameVersions.findIndex((v) => v.version === FIRST_QUICK_PLAY_VERSION)
|
const targetIndex = gameVersions.findIndex((v) => v.version === '23w14a')
|
||||||
|
|
||||||
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
||||||
}
|
}
|
||||||
|
|||||||
@ -383,11 +383,11 @@
|
|||||||
"instance.worlds.no_contact": {
|
"instance.worlds.no_contact": {
|
||||||
"message": "Server couldn't be contacted"
|
"message": "Server couldn't be contacted"
|
||||||
},
|
},
|
||||||
"instance.worlds.no_quick_play": {
|
"instance.worlds.no_server_quick_play": {
|
||||||
"message": "You can only jump straight into worlds on Minecraft 1.20+"
|
"message": "You can only jump straight into servers on Minecraft Alpha 1.0.5+"
|
||||||
},
|
},
|
||||||
"instance.worlds.play_anyway": {
|
"instance.worlds.no_singleplayer_quick_play": {
|
||||||
"message": "Play anyway"
|
"message": "You can only jump straight into singleplayer worlds on Minecraft 1.20+"
|
||||||
},
|
},
|
||||||
"instance.worlds.play_instance": {
|
"instance.worlds.play_instance": {
|
||||||
"message": "Play instance"
|
"message": "Play instance"
|
||||||
|
|||||||
@ -67,7 +67,8 @@
|
|||||||
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
||||||
:world="world"
|
:world="world"
|
||||||
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
||||||
:supports-quick-play="supportsQuickPlay"
|
:supports-server-quick-play="supportsServerQuickPlay"
|
||||||
|
:supports-world-quick-play="supportsWorldQuickPlay"
|
||||||
:current-protocol="protocolVersion"
|
:current-protocol="protocolVersion"
|
||||||
:playing-instance="playing"
|
:playing-instance="playing"
|
||||||
:playing-world="worldsMatch(world, worldPlaying)"
|
:playing-world="worldsMatch(world, worldPlaying)"
|
||||||
@ -144,12 +145,14 @@ import {
|
|||||||
refreshServerData,
|
refreshServerData,
|
||||||
refreshServers,
|
refreshServers,
|
||||||
refreshWorld,
|
refreshWorld,
|
||||||
|
hasWorldQuickPlaySupport,
|
||||||
refreshWorlds,
|
refreshWorlds,
|
||||||
remove_server_from_profile,
|
remove_server_from_profile,
|
||||||
showWorldInFolder,
|
showWorldInFolder,
|
||||||
sortWorlds,
|
sortWorlds,
|
||||||
start_join_server,
|
start_join_server,
|
||||||
start_join_singleplayer_world,
|
start_join_singleplayer_world,
|
||||||
|
hasServerQuickPlaySupport,
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import { PlusIcon, SearchIcon, SpinnerIcon, UpdatedIcon, XIcon } from '@modrinth/assets'
|
import { PlusIcon, SearchIcon, SpinnerIcon, UpdatedIcon, XIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
@ -355,8 +358,11 @@ function worldsMatch(world: World, other: World | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []))
|
const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []))
|
||||||
const supportsQuickPlay = computed(() =>
|
const supportsServerQuickPlay = computed(() =>
|
||||||
hasQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||||
|
)
|
||||||
|
const supportsWorldQuickPlay = computed(() =>
|
||||||
|
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||||
)
|
)
|
||||||
|
|
||||||
const filterOptions = computed(() => {
|
const filterOptions = computed(() => {
|
||||||
|
|||||||
@ -250,7 +250,7 @@ pub async fn profile_get_pack_export_candidates(
|
|||||||
// invoke('plugin:profile|profile_run', path)
|
// invoke('plugin:profile|profile_run', path)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
||||||
let process = profile::run(path, &QuickPlayType::None).await?;
|
let process = profile::run(path, QuickPlayType::None).await?;
|
||||||
|
|
||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ use enumset::EnumSet;
|
|||||||
use tauri::{AppHandle, Manager, Runtime};
|
use tauri::{AppHandle, Manager, Runtime};
|
||||||
use theseus::prelude::ProcessMetadata;
|
use theseus::prelude::ProcessMetadata;
|
||||||
use theseus::profile::{QuickPlayType, get_full_path};
|
use theseus::profile::{QuickPlayType, get_full_path};
|
||||||
|
use theseus::server_address::ServerAddress;
|
||||||
use theseus::worlds::{
|
use theseus::worlds::{
|
||||||
DisplayStatus, ProtocolVersion, ServerPackStatus, ServerStatus, World,
|
DisplayStatus, ProtocolVersion, ServerPackStatus, ServerStatus, World,
|
||||||
WorldType, WorldWithProfile,
|
WorldType, WorldWithProfile,
|
||||||
@ -203,7 +204,7 @@ pub async fn start_join_singleplayer_world(
|
|||||||
world: String,
|
world: String,
|
||||||
) -> Result<ProcessMetadata> {
|
) -> Result<ProcessMetadata> {
|
||||||
let process =
|
let process =
|
||||||
profile::run(path, &QuickPlayType::Singleplayer(world)).await?;
|
profile::run(path, QuickPlayType::Singleplayer(world)).await?;
|
||||||
|
|
||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
@ -213,8 +214,11 @@ pub async fn start_join_server(
|
|||||||
path: &str,
|
path: &str,
|
||||||
address: &str,
|
address: &str,
|
||||||
) -> Result<ProcessMetadata> {
|
) -> Result<ProcessMetadata> {
|
||||||
let process =
|
let process = profile::run(
|
||||||
profile::run(path, &QuickPlayType::Server(address.to_owned())).await?;
|
path,
|
||||||
|
QuickPlayType::Server(ServerAddress::Unresolved(address.to_owned())),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
<div class="static w-full grid-cols-1 md:relative md:flex">
|
<div class="static w-full grid-cols-1 md:relative md:flex">
|
||||||
<div class="static h-full flex-col pb-4 md:flex md:pb-0 md:pr-4">
|
<div class="static h-full flex-col pb-4 md:flex md:pb-0 md:pr-4">
|
||||||
<div class="z-10 flex select-none flex-col gap-2 rounded-2xl bg-bg-raised p-4 md:w-[16rem]">
|
<div class="z-10 flex select-none flex-col gap-2 rounded-2xl bg-bg-raised p-4 md:w-[16rem]">
|
||||||
<div v-for="link in navLinks" :key="link.label">
|
<div
|
||||||
|
v-for="link in navLinks.filter((x) => x.shown === undefined || x.shown)"
|
||||||
|
:key="link.label"
|
||||||
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="link.href"
|
:to="link.href"
|
||||||
class="flex items-center gap-2 rounded-xl p-2 hover:bg-button-bg"
|
class="flex items-center gap-2 rounded-xl p-2 hover:bg-button-bg"
|
||||||
@ -40,7 +43,7 @@ import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|||||||
const emit = defineEmits(["reinstall"]);
|
const emit = defineEmits(["reinstall"]);
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
|
navLinks: { label: string; href: string; icon: Component; external?: boolean; shown?: boolean }[];
|
||||||
route: RouteLocationNormalized;
|
route: RouteLocationNormalized;
|
||||||
server: ModrinthServer;
|
server: ModrinthServer;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { ServerModule } from "./base.ts";
|
|||||||
export class GeneralModule extends ServerModule implements ServerGeneral {
|
export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||||
server_id!: string;
|
server_id!: string;
|
||||||
name!: string;
|
name!: string;
|
||||||
|
owner_id!: string;
|
||||||
net!: { ip: string; port: number; domain: string };
|
net!: { ip: string; port: number; domain: string };
|
||||||
game!: string;
|
game!: string;
|
||||||
backup_quota!: number;
|
backup_quota!: number;
|
||||||
|
|||||||
@ -16,12 +16,15 @@ import {
|
|||||||
CardIcon,
|
CardIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
|
ModrinthIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
|
import { isAdmin as isUserAdmin, type User } from "@modrinth/utils";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string;
|
||||||
|
const auth = await useAuth();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: ModrinthServer;
|
||||||
@ -32,7 +35,11 @@ useHead({
|
|||||||
title: `Options - ${props.server.general?.name ?? "Server"} - Modrinth`,
|
title: `Options - ${props.server.general?.name ?? "Server"} - Modrinth`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const navLinks = [
|
const ownerId = computed(() => props.server.general?.owner_id ?? "Ghost");
|
||||||
|
const isOwner = computed(() => (auth.value?.user as User | null)?.id === ownerId.value);
|
||||||
|
const isAdmin = computed(() => isUserAdmin(auth.value?.user));
|
||||||
|
|
||||||
|
const navLinks = computed(() => [
|
||||||
{ icon: SettingsIcon, label: "General", href: `/servers/manage/${serverId}/options` },
|
{ icon: SettingsIcon, label: "General", href: `/servers/manage/${serverId}/options` },
|
||||||
{ icon: WrenchIcon, label: "Platform", href: `/servers/manage/${serverId}/options/loader` },
|
{ icon: WrenchIcon, label: "Platform", href: `/servers/manage/${serverId}/options/loader` },
|
||||||
{ icon: TextQuoteIcon, label: "Startup", href: `/servers/manage/${serverId}/options/startup` },
|
{ icon: TextQuoteIcon, label: "Startup", href: `/servers/manage/${serverId}/options/startup` },
|
||||||
@ -48,7 +55,15 @@ const navLinks = [
|
|||||||
label: "Billing",
|
label: "Billing",
|
||||||
href: `/settings/billing#server-${serverId}`,
|
href: `/settings/billing#server-${serverId}`,
|
||||||
external: true,
|
external: true,
|
||||||
|
shown: isOwner.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ModrinthIcon,
|
||||||
|
label: "Admin Billing",
|
||||||
|
href: `/admin/billing/${ownerId.value}`,
|
||||||
|
external: true,
|
||||||
|
shown: isAdmin.value,
|
||||||
},
|
},
|
||||||
{ icon: InfoIcon, label: "Info", href: `/servers/manage/${serverId}/options/info` },
|
{ icon: InfoIcon, label: "Info", href: `/servers/manage/${serverId}/options/info` },
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -53,6 +53,7 @@ fn build_java_jars() {
|
|||||||
.arg("build")
|
.arg("build")
|
||||||
.arg("--no-daemon")
|
.arg("--no-daemon")
|
||||||
.arg("--console=rich")
|
.arg("--console=rich")
|
||||||
|
.arg("--info")
|
||||||
.current_dir(dunce::canonicalize("java").unwrap())
|
.current_dir(dunce::canonicalize("java").unwrap())
|
||||||
.status()
|
.status()
|
||||||
.expect("Failed to wait on Gradle build");
|
.expect("Failed to wait on Gradle build");
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("com.diffplug.spotless") version "7.0.4"
|
id("com.diffplug.spotless") version "7.0.4"
|
||||||
|
id("com.gradleup.shadow") version "9.0.0-rc2"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@ -8,6 +9,9 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("org.ow2.asm:asm:9.8")
|
||||||
|
implementation("org.ow2.asm:asm-tree:9.8")
|
||||||
|
|
||||||
testImplementation(libs.junit.jupiter)
|
testImplementation(libs.junit.jupiter)
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
}
|
}
|
||||||
@ -31,7 +35,17 @@ spotless {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.jar {
|
tasks.jar {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
archiveFileName = "theseus.jar"
|
archiveFileName = "theseus.jar"
|
||||||
|
manifest {
|
||||||
|
attributes["Premain-Class"] = "com.modrinth.theseus.agent.TheseusAgent"
|
||||||
|
}
|
||||||
|
|
||||||
|
enableRelocation = true
|
||||||
|
relocationPrefix = "com.modrinth.theseus.shadow"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<Test>("test") {
|
tasks.named<Test>("test") {
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.modrinth.theseus.agent;
|
||||||
|
|
||||||
|
import java.util.ListIterator;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import org.objectweb.asm.Type;
|
||||||
|
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||||
|
import org.objectweb.asm.tree.FieldInsnNode;
|
||||||
|
|
||||||
|
public interface InsnPattern extends Predicate<AbstractInsnNode> {
|
||||||
|
/**
|
||||||
|
* Advances past the first match of all instructions in the pattern.
|
||||||
|
* @return {@code true} if the pattern was found, {@code false} if not
|
||||||
|
*/
|
||||||
|
static boolean findAndSkip(ListIterator<AbstractInsnNode> iterator, InsnPattern... pattern) {
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int patternIndex = 0;
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
final AbstractInsnNode insn = iterator.next();
|
||||||
|
if (insn.getOpcode() == -1) continue;
|
||||||
|
if (pattern[patternIndex].test(insn) && ++patternIndex == pattern.length) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
patternIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static InsnPattern opcode(int opcode) {
|
||||||
|
return insn -> insn.getOpcode() == opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static InsnPattern field(int opcode, Type fieldType) {
|
||||||
|
final String typeDescriptor = fieldType.getDescriptor();
|
||||||
|
return insn -> {
|
||||||
|
if (insn.getOpcode() != opcode || !(insn instanceof FieldInsnNode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final FieldInsnNode fieldInsn = (FieldInsnNode) insn;
|
||||||
|
return typeDescriptor.equals(fieldInsn.desc);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.modrinth.theseus.agent;
|
||||||
|
|
||||||
|
// Must be kept up-to-date with quick_play_version.rs
|
||||||
|
public enum QuickPlayServerVersion {
|
||||||
|
BUILTIN,
|
||||||
|
BUILTIN_LEGACY,
|
||||||
|
INJECTED,
|
||||||
|
UNSUPPORTED;
|
||||||
|
|
||||||
|
public static final QuickPlayServerVersion CURRENT =
|
||||||
|
valueOf(System.getProperty("modrinth.internal.quickPlay.serverVersion"));
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
package com.modrinth.theseus.agent;
|
||||||
|
|
||||||
|
import com.modrinth.theseus.agent.transformers.ClassTransformer;
|
||||||
|
import com.modrinth.theseus.agent.transformers.MinecraftTransformer;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
|
||||||
|
@SuppressWarnings({"NullableProblems", "CallToPrintStackTrace"})
|
||||||
|
public final class TheseusAgent {
|
||||||
|
private static final boolean DEBUG_AGENT = Boolean.getBoolean("modrinth.debugAgent");
|
||||||
|
|
||||||
|
public static void premain(String args, Instrumentation instrumentation) {
|
||||||
|
final Path debugPath = Paths.get("ModrinthDebugTransformed");
|
||||||
|
if (DEBUG_AGENT) {
|
||||||
|
System.out.println(
|
||||||
|
"===== Theseus agent debugging enabled. Dumping transformed classes to " + debugPath + " =====");
|
||||||
|
if (Files.exists(debugPath)) {
|
||||||
|
try {
|
||||||
|
Files.walkFileTree(debugPath, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
Files.delete(file);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||||
|
Files.delete(dir);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
new UncheckedIOException("Failed to delete " + debugPath, e).printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("===== Quick play server version: " + QuickPlayServerVersion.CURRENT + " =====");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, ClassTransformer> transformers = new HashMap<>();
|
||||||
|
transformers.put("net/minecraft/client/Minecraft", new MinecraftTransformer());
|
||||||
|
|
||||||
|
instrumentation.addTransformer((loader, className, classBeingRedefined, protectionDomain, classData) -> {
|
||||||
|
final ClassTransformer transformer = transformers.get(className);
|
||||||
|
if (transformer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final ClassReader reader = new ClassReader(classData);
|
||||||
|
final ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
|
||||||
|
try {
|
||||||
|
if (!transformer.transform(reader, writer)) {
|
||||||
|
if (DEBUG_AGENT) {
|
||||||
|
System.out.println("Not writing " + className + " as its transformer returned false");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
new IllegalStateException("Failed to transform " + className, t).printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final byte[] result = writer.toByteArray();
|
||||||
|
if (DEBUG_AGENT) {
|
||||||
|
try {
|
||||||
|
final Path path = debugPath.resolve(className + ".class");
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.write(path, result);
|
||||||
|
System.out.println("Dumped class to " + path.toAbsolutePath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
new UncheckedIOException("Failed to dump class " + className, e).printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.modrinth.theseus.agent.transformers;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
|
||||||
|
public abstract class ClassNodeTransformer extends ClassTransformer {
|
||||||
|
protected abstract boolean transform(ClassNode classNode);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean transform(ClassReader reader, ClassWriter writer) {
|
||||||
|
final ClassNode classNode = new ClassNode();
|
||||||
|
reader.accept(classNode, 0);
|
||||||
|
if (!transform(classNode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
classNode.accept(writer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.modrinth.theseus.agent.transformers;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
|
||||||
|
public abstract class ClassTransformer {
|
||||||
|
public abstract boolean transform(ClassReader reader, ClassWriter writer);
|
||||||
|
|
||||||
|
protected static boolean needsStackMap(ClassNode classNode) {
|
||||||
|
return (classNode.version & 0xffff) >= Opcodes.V1_6;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
package com.modrinth.theseus.agent.transformers;
|
||||||
|
|
||||||
|
import com.modrinth.theseus.agent.InsnPattern;
|
||||||
|
import com.modrinth.theseus.agent.QuickPlayServerVersion;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import org.objectweb.asm.tree.FrameNode;
|
||||||
|
import org.objectweb.asm.tree.InsnNode;
|
||||||
|
import org.objectweb.asm.tree.JumpInsnNode;
|
||||||
|
import org.objectweb.asm.tree.LabelNode;
|
||||||
|
import org.objectweb.asm.tree.LdcInsnNode;
|
||||||
|
import org.objectweb.asm.tree.MethodInsnNode;
|
||||||
|
import org.objectweb.asm.tree.MethodNode;
|
||||||
|
import org.objectweb.asm.tree.VarInsnNode;
|
||||||
|
|
||||||
|
public final class MinecraftTransformer extends ClassNodeTransformer {
|
||||||
|
private static final String SET_SERVER_NAME_DESC = "(Ljava/lang/String;I)V";
|
||||||
|
private static final InsnPattern[] INITIALIZE_THIS_PATTERN = {InsnPattern.opcode(Opcodes.INVOKESPECIAL)};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean transform(ClassNode classNode) {
|
||||||
|
if (QuickPlayServerVersion.CURRENT == QuickPlayServerVersion.INJECTED) {
|
||||||
|
return addServerJoinSupport(classNode);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean addServerJoinSupport(ClassNode classNode) {
|
||||||
|
String setServerName = null;
|
||||||
|
MethodNode constructor = null;
|
||||||
|
for (final MethodNode method : classNode.methods) {
|
||||||
|
if (constructor == null && method.name.equals("<init>")) {
|
||||||
|
constructor = method;
|
||||||
|
} else if (method.desc.equals(SET_SERVER_NAME_DESC) && method.name.indexOf('$') == -1) {
|
||||||
|
// Check for $ is because Mixin-injected methods should have $ in it
|
||||||
|
if (setServerName == null) {
|
||||||
|
setServerName = method.name;
|
||||||
|
} else {
|
||||||
|
// Already found a setServer method, but we found another one? Since we can't
|
||||||
|
// know which is real, just return so we don't call something we shouldn't.
|
||||||
|
// Note this can't happen unless some other mod is adding a method with this
|
||||||
|
// same descriptor.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (constructor == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ListIterator<AbstractInsnNode> it = constructor.instructions.iterator();
|
||||||
|
if (!InsnPattern.findAndSkip(it, INITIALIZE_THIS_PATTERN)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final LabelNode noQuickPlayLabel = new LabelNode();
|
||||||
|
final LabelNode doneQuickPlayLabel = new LabelNode();
|
||||||
|
it.add(new LdcInsnNode("modrinth.internal.quickPlay.host"));
|
||||||
|
// String
|
||||||
|
it.add(new MethodInsnNode(
|
||||||
|
Opcodes.INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;"));
|
||||||
|
// String
|
||||||
|
it.add(new InsnNode(Opcodes.DUP));
|
||||||
|
// String String
|
||||||
|
it.add(new JumpInsnNode(Opcodes.IFNULL, noQuickPlayLabel));
|
||||||
|
// String
|
||||||
|
it.add(new VarInsnNode(Opcodes.ALOAD, 0));
|
||||||
|
// String Minecraft
|
||||||
|
it.add(new InsnNode(Opcodes.SWAP));
|
||||||
|
// Minecraft String
|
||||||
|
it.add(new LdcInsnNode("modrinth.internal.quickPlay.port"));
|
||||||
|
// Minecraft String String
|
||||||
|
it.add(new MethodInsnNode(
|
||||||
|
Opcodes.INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;"));
|
||||||
|
// Minecraft String String
|
||||||
|
it.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/Integer", "parseInt", "(Ljava/lang/String;)I"));
|
||||||
|
// Minecraft String int
|
||||||
|
it.add(new MethodInsnNode(
|
||||||
|
Opcodes.INVOKEVIRTUAL, "net/minecraft/client/Minecraft", setServerName, SET_SERVER_NAME_DESC));
|
||||||
|
//
|
||||||
|
it.add(new JumpInsnNode(Opcodes.GOTO, doneQuickPlayLabel));
|
||||||
|
it.add(noQuickPlayLabel);
|
||||||
|
if (needsStackMap(classNode)) {
|
||||||
|
it.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
|
||||||
|
}
|
||||||
|
// String
|
||||||
|
it.add(new InsnNode(Opcodes.POP));
|
||||||
|
//
|
||||||
|
it.add(doneQuickPlayLabel);
|
||||||
|
if (needsStackMap(classNode)) {
|
||||||
|
it.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
|
||||||
|
}
|
||||||
|
//
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ pub mod mr_auth;
|
|||||||
pub mod pack;
|
pub mod pack;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
pub mod server_address;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod worlds;
|
pub mod worlds;
|
||||||
|
|||||||
@ -23,6 +23,7 @@ use serde_json::json;
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::data::Settings;
|
use crate::data::Settings;
|
||||||
|
use crate::server_address::ServerAddress;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
use std::{
|
use std::{
|
||||||
@ -40,7 +41,7 @@ pub mod update;
|
|||||||
pub enum QuickPlayType {
|
pub enum QuickPlayType {
|
||||||
None,
|
None,
|
||||||
Singleplayer(String),
|
Singleplayer(String),
|
||||||
Server(String),
|
Server(ServerAddress),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a profile
|
/// Remove a profile
|
||||||
@ -630,7 +631,7 @@ fn pack_get_relative_path(
|
|||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
path: &str,
|
path: &str,
|
||||||
quick_play_type: &QuickPlayType,
|
quick_play_type: QuickPlayType,
|
||||||
) -> crate::Result<ProcessMetadata> {
|
) -> crate::Result<ProcessMetadata> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
|
||||||
@ -646,7 +647,7 @@ pub async fn run(
|
|||||||
async fn run_credentials(
|
async fn run_credentials(
|
||||||
path: &str,
|
path: &str,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
quick_play_type: &QuickPlayType,
|
quick_play_type: QuickPlayType,
|
||||||
) -> crate::Result<ProcessMetadata> {
|
) -> crate::Result<ProcessMetadata> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let settings = Settings::get(&state.pool).await?;
|
let settings = Settings::get(&state.pool).await?;
|
||||||
|
|||||||
166
packages/app-lib/src/api/server_address.rs
Normal file
166
packages/app-lib/src/api/server_address.rs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::mem;
|
||||||
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ServerAddress {
|
||||||
|
Unresolved(String),
|
||||||
|
Resolved {
|
||||||
|
original_host: String,
|
||||||
|
original_port: u16,
|
||||||
|
resolved_host: String,
|
||||||
|
resolved_port: u16,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerAddress {
|
||||||
|
pub async fn resolve(&mut self) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Unresolved(address) => {
|
||||||
|
let (host, port) = parse_server_address(address)?;
|
||||||
|
let (resolved_host, resolved_port) =
|
||||||
|
resolve_server_address(host, port).await?;
|
||||||
|
*self = Self::Resolved {
|
||||||
|
original_host: if host.len() == address.len() {
|
||||||
|
mem::take(address)
|
||||||
|
} else {
|
||||||
|
host.to_owned()
|
||||||
|
},
|
||||||
|
original_port: port,
|
||||||
|
resolved_host,
|
||||||
|
resolved_port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Resolved { .. } => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn require_resolved(&self) -> Result<(&str, u16)> {
|
||||||
|
match self {
|
||||||
|
Self::Resolved {
|
||||||
|
resolved_host,
|
||||||
|
resolved_port,
|
||||||
|
..
|
||||||
|
} => Ok((resolved_host, *resolved_port)),
|
||||||
|
Self::Unresolved(address) => Err(ErrorKind::InputError(format!(
|
||||||
|
"Unexpected unresolved server address: {address}"
|
||||||
|
))
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ServerAddress {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Unresolved(address) => write!(f, "{address}"),
|
||||||
|
Self::Resolved {
|
||||||
|
resolved_host,
|
||||||
|
resolved_port,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if resolved_host.contains(':') {
|
||||||
|
write!(f, "[{resolved_host}]:{resolved_port}")
|
||||||
|
} else {
|
||||||
|
write!(f, "{resolved_host}:{resolved_port}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_server_address(address: &str) -> Result<(&str, u16)> {
|
||||||
|
parse_server_address_inner(address)
|
||||||
|
.map_err(|e| Error::from(ErrorKind::InputError(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reimplementation of Guava's HostAndPort#fromString with a default port of 25565
|
||||||
|
fn parse_server_address_inner(
|
||||||
|
address: &str,
|
||||||
|
) -> std::result::Result<(&str, u16), String> {
|
||||||
|
let (host, port_str) = if address.starts_with("[") {
|
||||||
|
let colon_index = address.find(':');
|
||||||
|
let close_bracket_index = address.rfind(']');
|
||||||
|
if colon_index.is_none() || close_bracket_index.is_none() {
|
||||||
|
return Err(format!("Invalid bracketed host/port: {address}"));
|
||||||
|
}
|
||||||
|
let close_bracket_index = close_bracket_index.unwrap();
|
||||||
|
|
||||||
|
let host = &address[1..close_bracket_index];
|
||||||
|
if close_bracket_index + 1 == address.len() {
|
||||||
|
(host, "")
|
||||||
|
} else {
|
||||||
|
if address.as_bytes().get(close_bracket_index).copied()
|
||||||
|
!= Some(b':')
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Only a colon may follow a close bracket: {address}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let port_str = &address[close_bracket_index + 2..];
|
||||||
|
for c in port_str.chars() {
|
||||||
|
if !c.is_ascii_digit() {
|
||||||
|
return Err(format!("Port must be numeric: {address}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(host, port_str)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let colon_pos = address.find(':');
|
||||||
|
if let Some(colon_pos) = colon_pos {
|
||||||
|
(&address[..colon_pos], &address[colon_pos + 1..])
|
||||||
|
} else {
|
||||||
|
(address, "")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut port = None;
|
||||||
|
if !port_str.is_empty() {
|
||||||
|
if port_str.starts_with('+') {
|
||||||
|
return Err(format!("Unparseable port number: {port_str}"));
|
||||||
|
}
|
||||||
|
port = port_str.parse::<u16>().ok();
|
||||||
|
if port.is_none() {
|
||||||
|
return Err(format!("Unparseable port number: {port_str}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((host, port.unwrap_or(25565)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve_server_address(
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
) -> Result<(String, u16)> {
|
||||||
|
static SIMULTANEOUS_DNS_QUERIES: Semaphore = Semaphore::const_new(24);
|
||||||
|
|
||||||
|
if port != 25565
|
||||||
|
|| host.parse::<Ipv4Addr>().is_ok()
|
||||||
|
|| host.parse::<Ipv6Addr>().is_ok()
|
||||||
|
{
|
||||||
|
return Ok((host.to_owned(), port));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _permit = SIMULTANEOUS_DNS_QUERIES.acquire().await?;
|
||||||
|
let resolver = hickory_resolver::TokioResolver::builder_tokio()?.build();
|
||||||
|
Ok(
|
||||||
|
match resolver.srv_lookup(format!("_minecraft._tcp.{host}")).await {
|
||||||
|
Err(e)
|
||||||
|
if e.proto()
|
||||||
|
.filter(|x| x.kind().is_no_records_found())
|
||||||
|
.is_some() =>
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
Ok(lookup) => lookup
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(|r| (r.target().to_string(), r.port())),
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| (host.to_owned(), port)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::data::ModLoader;
|
use crate::data::ModLoader;
|
||||||
use crate::launcher::get_loader_version_from_profile;
|
use crate::launcher::get_loader_version_from_profile;
|
||||||
use crate::profile::get_full_path;
|
use crate::profile::get_full_path;
|
||||||
|
use crate::server_address::{parse_server_address, resolve_server_address};
|
||||||
use crate::state::attached_world_data::AttachedWorldData;
|
use crate::state::attached_world_data::AttachedWorldData;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
||||||
@ -11,7 +12,7 @@ pub use crate::util::server_ping::{
|
|||||||
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
|
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
|
||||||
};
|
};
|
||||||
use crate::util::{io, server_ping};
|
use crate::util::{io, server_ping};
|
||||||
use crate::{Error, ErrorKind, Result, State, launcher};
|
use crate::{ErrorKind, Result, State, launcher};
|
||||||
use async_walkdir::WalkDir;
|
use async_walkdir::WalkDir;
|
||||||
use async_zip::{Compression, ZipEntryBuilder};
|
use async_zip::{Compression, ZipEntryBuilder};
|
||||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||||
@ -24,11 +25,9 @@ use regex::{Regex, RegexBuilder};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@ -433,9 +432,9 @@ async fn get_server_worlds_in_profile(
|
|||||||
let mut futures = JoinSet::new();
|
let mut futures = JoinSet::new();
|
||||||
for (index, world) in worlds.iter().enumerate().skip(first_server_index)
|
for (index, world) in worlds.iter().enumerate().skip(first_server_index)
|
||||||
{
|
{
|
||||||
if world.last_played.is_some() {
|
// We can't check for the profile already having a last_played, in case the user joined
|
||||||
continue;
|
// the target address directly more recently. This is often the case when using
|
||||||
}
|
// quick-play before 1.20.
|
||||||
if let WorldDetails::Server { address, .. } = &world.details
|
if let WorldDetails::Server { address, .. } = &world.details
|
||||||
&& let Ok((host, port)) = parse_server_address(address)
|
&& let Ok((host, port)) = parse_server_address(address)
|
||||||
{
|
{
|
||||||
@ -917,93 +916,3 @@ pub async fn get_server_status(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_server_address(address: &str) -> Result<(&str, u16)> {
|
|
||||||
parse_server_address_inner(address)
|
|
||||||
.map_err(|e| Error::from(ErrorKind::InputError(e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reimplementation of Guava's HostAndPort#fromString with a default port of 25565
|
|
||||||
fn parse_server_address_inner(
|
|
||||||
address: &str,
|
|
||||||
) -> std::result::Result<(&str, u16), String> {
|
|
||||||
let (host, port_str) = if address.starts_with("[") {
|
|
||||||
let colon_index = address.find(':');
|
|
||||||
let close_bracket_index = address.rfind(']');
|
|
||||||
if colon_index.is_none() || close_bracket_index.is_none() {
|
|
||||||
return Err(format!("Invalid bracketed host/port: {address}"));
|
|
||||||
}
|
|
||||||
let close_bracket_index = close_bracket_index.unwrap();
|
|
||||||
|
|
||||||
let host = &address[1..close_bracket_index];
|
|
||||||
if close_bracket_index + 1 == address.len() {
|
|
||||||
(host, "")
|
|
||||||
} else {
|
|
||||||
if address.as_bytes().get(close_bracket_index).copied()
|
|
||||||
!= Some(b':')
|
|
||||||
{
|
|
||||||
return Err(format!(
|
|
||||||
"Only a colon may follow a close bracket: {address}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let port_str = &address[close_bracket_index + 2..];
|
|
||||||
for c in port_str.chars() {
|
|
||||||
if !c.is_ascii_digit() {
|
|
||||||
return Err(format!("Port must be numeric: {address}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(host, port_str)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let colon_pos = address.find(':');
|
|
||||||
if let Some(colon_pos) = colon_pos {
|
|
||||||
(&address[..colon_pos], &address[colon_pos + 1..])
|
|
||||||
} else {
|
|
||||||
(address, "")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut port = None;
|
|
||||||
if !port_str.is_empty() {
|
|
||||||
if port_str.starts_with('+') {
|
|
||||||
return Err(format!("Unparseable port number: {port_str}"));
|
|
||||||
}
|
|
||||||
port = port_str.parse::<u16>().ok();
|
|
||||||
if port.is_none() {
|
|
||||||
return Err(format!("Unparseable port number: {port_str}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((host, port.unwrap_or(25565)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn resolve_server_address(
|
|
||||||
host: &str,
|
|
||||||
port: u16,
|
|
||||||
) -> Result<(String, u16)> {
|
|
||||||
static SIMULTANEOUS_DNS_QUERIES: Semaphore = Semaphore::const_new(24);
|
|
||||||
|
|
||||||
if host.parse::<Ipv4Addr>().is_ok() || host.parse::<Ipv6Addr>().is_ok() {
|
|
||||||
return Ok((host.to_owned(), port));
|
|
||||||
}
|
|
||||||
|
|
||||||
let _permit = SIMULTANEOUS_DNS_QUERIES.acquire().await?;
|
|
||||||
let resolver = hickory_resolver::TokioResolver::builder_tokio()?.build();
|
|
||||||
Ok(
|
|
||||||
match resolver.srv_lookup(format!("_minecraft._tcp.{host}")).await {
|
|
||||||
Err(e)
|
|
||||||
if e.proto()
|
|
||||||
.filter(|x| x.kind().is_no_records_found())
|
|
||||||
.is_some() =>
|
|
||||||
{
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
Ok(lookup) => lookup
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.map(|r| (r.target().to_string(), r.port())),
|
|
||||||
}
|
|
||||||
.unwrap_or_else(|| (host.to_owned(), port)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
//! Minecraft CLI argument logic
|
//! Minecraft CLI argument logic
|
||||||
use crate::launcher::parse_rules;
|
use crate::launcher::quick_play_version::QuickPlayServerVersion;
|
||||||
|
use crate::launcher::{QuickPlayVersion, parse_rules};
|
||||||
use crate::profile::QuickPlayType;
|
use crate::profile::QuickPlayType;
|
||||||
use crate::state::Credentials;
|
use crate::state::Credentials;
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -115,11 +116,13 @@ pub fn get_jvm_arguments(
|
|||||||
libraries_path: &Path,
|
libraries_path: &Path,
|
||||||
log_configs_path: &Path,
|
log_configs_path: &Path,
|
||||||
class_paths: &str,
|
class_paths: &str,
|
||||||
|
agent_path: &Path,
|
||||||
version_name: &str,
|
version_name: &str,
|
||||||
memory: MemorySettings,
|
memory: MemorySettings,
|
||||||
custom_args: Vec<String>,
|
custom_args: Vec<String>,
|
||||||
java_arch: &str,
|
java_arch: &str,
|
||||||
quick_play_type: &QuickPlayType,
|
quick_play_type: &QuickPlayType,
|
||||||
|
quick_play_version: QuickPlayVersion,
|
||||||
log_config: Option<&LoggingConfiguration>,
|
log_config: Option<&LoggingConfiguration>,
|
||||||
) -> crate::Result<Vec<String>> {
|
) -> crate::Result<Vec<String>> {
|
||||||
let mut parsed_arguments = Vec::new();
|
let mut parsed_arguments = Vec::new();
|
||||||
@ -155,13 +158,45 @@ pub fn get_jvm_arguments(
|
|||||||
parsed_arguments.push("-cp".to_string());
|
parsed_arguments.push("-cp".to_string());
|
||||||
parsed_arguments.push(class_paths.to_string());
|
parsed_arguments.push(class_paths.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed_arguments.push(format!("-Xmx{}M", memory.maximum));
|
parsed_arguments.push(format!("-Xmx{}M", memory.maximum));
|
||||||
|
|
||||||
if let Some(LoggingConfiguration::Log4j2Xml { argument, file }) = log_config
|
if let Some(LoggingConfiguration::Log4j2Xml { argument, file }) = log_config
|
||||||
{
|
{
|
||||||
let full_path = log_configs_path.join(&file.id);
|
let full_path = log_configs_path.join(&file.id);
|
||||||
let full_path = full_path.to_string_lossy();
|
let full_path = full_path.to_string_lossy();
|
||||||
parsed_arguments.push(argument.replace("${path}", &full_path));
|
parsed_arguments.push(argument.replace("${path}", &full_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsed_arguments.push(format!(
|
||||||
|
"-javaagent:{}",
|
||||||
|
canonicalize(agent_path)
|
||||||
|
.map_err(|_| {
|
||||||
|
crate::ErrorKind::LauncherError(format!(
|
||||||
|
"Specified Java Agent path {} does not exist",
|
||||||
|
libraries_path.to_string_lossy()
|
||||||
|
))
|
||||||
|
.as_error()
|
||||||
|
})?
|
||||||
|
.to_string_lossy()
|
||||||
|
));
|
||||||
|
|
||||||
|
parsed_arguments.push(format!(
|
||||||
|
"-Dmodrinth.internal.quickPlay.serverVersion={}",
|
||||||
|
serde_json::to_value(quick_play_version.server)?
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
));
|
||||||
|
if let QuickPlayType::Server(server) = quick_play_type
|
||||||
|
&& quick_play_version.server == QuickPlayServerVersion::Injected
|
||||||
|
{
|
||||||
|
let (host, port) = server.require_resolved()?;
|
||||||
|
parsed_arguments.extend_from_slice(&[
|
||||||
|
format!("-Dmodrinth.internal.quickPlay.host={host}"),
|
||||||
|
format!("-Dmodrinth.internal.quickPlay.port={port}"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
for arg in custom_args {
|
for arg in custom_args {
|
||||||
if !arg.is_empty() {
|
if !arg.is_empty() {
|
||||||
parsed_arguments.push(arg);
|
parsed_arguments.push(arg);
|
||||||
@ -225,13 +260,13 @@ pub async fn get_minecraft_arguments(
|
|||||||
resolution: WindowSize,
|
resolution: WindowSize,
|
||||||
java_arch: &str,
|
java_arch: &str,
|
||||||
quick_play_type: &QuickPlayType,
|
quick_play_type: &QuickPlayType,
|
||||||
|
quick_play_version: QuickPlayVersion,
|
||||||
) -> crate::Result<Vec<String>> {
|
) -> crate::Result<Vec<String>> {
|
||||||
let access_token = credentials.access_token.clone();
|
let access_token = credentials.access_token.clone();
|
||||||
let profile = credentials.maybe_online_profile().await;
|
let profile = credentials.maybe_online_profile().await;
|
||||||
|
|
||||||
if let Some(arguments) = arguments {
|
|
||||||
let mut parsed_arguments = Vec::new();
|
let mut parsed_arguments = Vec::new();
|
||||||
|
|
||||||
|
if let Some(arguments) = arguments {
|
||||||
parse_arguments(
|
parse_arguments(
|
||||||
arguments,
|
arguments,
|
||||||
&mut parsed_arguments,
|
&mut parsed_arguments,
|
||||||
@ -253,10 +288,7 @@ pub async fn get_minecraft_arguments(
|
|||||||
java_arch,
|
java_arch,
|
||||||
quick_play_type,
|
quick_play_type,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(parsed_arguments)
|
|
||||||
} else if let Some(legacy_arguments) = legacy_arguments {
|
} else if let Some(legacy_arguments) = legacy_arguments {
|
||||||
let mut parsed_arguments = Vec::new();
|
|
||||||
for x in legacy_arguments.split(' ') {
|
for x in legacy_arguments.split(' ') {
|
||||||
parsed_arguments.push(parse_minecraft_argument(
|
parsed_arguments.push(parse_minecraft_argument(
|
||||||
&x.replace(' ', TEMPORARY_REPLACE_CHAR),
|
&x.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||||
@ -272,10 +304,21 @@ pub async fn get_minecraft_arguments(
|
|||||||
quick_play_type,
|
quick_play_type,
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
Ok(parsed_arguments)
|
|
||||||
} else {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let QuickPlayType::Server(server) = quick_play_type
|
||||||
|
&& quick_play_version.server == QuickPlayServerVersion::BuiltinLegacy
|
||||||
|
{
|
||||||
|
let (host, port) = server.require_resolved()?;
|
||||||
|
parsed_arguments.extend_from_slice(&[
|
||||||
|
"--server".to_string(),
|
||||||
|
host.to_string(),
|
||||||
|
"--port".to_string(),
|
||||||
|
port.to_string(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(parsed_arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@ -354,9 +397,9 @@ fn parse_minecraft_argument(
|
|||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"${quickPlayMultiplayer}",
|
"${quickPlayMultiplayer}",
|
||||||
match quick_play_type {
|
&match quick_play_type {
|
||||||
QuickPlayType::Server(address) => address,
|
QuickPlayType::Server(address) => address.to_string(),
|
||||||
_ => "",
|
_ => "".to_string(),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,9 @@ use crate::event::emit::{emit_loading, init_or_edit_loading};
|
|||||||
use crate::event::{LoadingBarId, LoadingBarType};
|
use crate::event::{LoadingBarId, LoadingBarType};
|
||||||
use crate::launcher::download::download_log_config;
|
use crate::launcher::download::download_log_config;
|
||||||
use crate::launcher::io::IOError;
|
use crate::launcher::io::IOError;
|
||||||
|
use crate::launcher::quick_play_version::{
|
||||||
|
QuickPlayServerVersion, QuickPlayVersion,
|
||||||
|
};
|
||||||
use crate::profile::QuickPlayType;
|
use crate::profile::QuickPlayType;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
||||||
@ -25,6 +28,7 @@ use tokio::process::Command;
|
|||||||
mod args;
|
mod args;
|
||||||
|
|
||||||
pub mod download;
|
pub mod download;
|
||||||
|
pub mod quick_play_version;
|
||||||
|
|
||||||
// All nones -> disallowed
|
// All nones -> disallowed
|
||||||
// 1+ true -> allowed
|
// 1+ true -> allowed
|
||||||
@ -457,7 +461,7 @@ pub async fn launch_minecraft(
|
|||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
post_exit_hook: Option<String>,
|
post_exit_hook: Option<String>,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
quick_play_type: &QuickPlayType,
|
mut quick_play_type: QuickPlayType,
|
||||||
) -> crate::Result<ProcessMetadata> {
|
) -> crate::Result<ProcessMetadata> {
|
||||||
if profile.install_stage == ProfileInstallStage::PackInstalling
|
if profile.install_stage == ProfileInstallStage::PackInstalling
|
||||||
|| profile.install_stage == ProfileInstallStage::MinecraftInstalling
|
|| profile.install_stage == ProfileInstallStage::MinecraftInstalling
|
||||||
@ -589,6 +593,18 @@ pub async fn launch_minecraft(
|
|||||||
io::create_dir_all(&natives_dir).await?;
|
io::create_dir_all(&natives_dir).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let quick_play_version =
|
||||||
|
QuickPlayVersion::find_version(version_index, &minecraft.versions);
|
||||||
|
tracing::debug!(
|
||||||
|
"Found QuickPlayVersion for {}: {quick_play_version:?}",
|
||||||
|
profile.game_version
|
||||||
|
);
|
||||||
|
if let QuickPlayType::Server(address) = &mut quick_play_type
|
||||||
|
&& quick_play_version.server >= QuickPlayServerVersion::BuiltinLegacy
|
||||||
|
{
|
||||||
|
address.resolve().await?;
|
||||||
|
}
|
||||||
|
|
||||||
let (main_class_keep_alive, main_class_path) =
|
let (main_class_keep_alive, main_class_path) =
|
||||||
get_resource_file!(env "JAVA_JARS_DIR" / "theseus.jar")?;
|
get_resource_file!(env "JAVA_JARS_DIR" / "theseus.jar")?;
|
||||||
|
|
||||||
@ -606,11 +622,13 @@ pub async fn launch_minecraft(
|
|||||||
&java_version.architecture,
|
&java_version.architecture,
|
||||||
minecraft_updated,
|
minecraft_updated,
|
||||||
)?,
|
)?,
|
||||||
|
&main_class_path,
|
||||||
&version_jar,
|
&version_jar,
|
||||||
*memory,
|
*memory,
|
||||||
Vec::from(java_args),
|
Vec::from(java_args),
|
||||||
&java_version.architecture,
|
&java_version.architecture,
|
||||||
quick_play_type,
|
&quick_play_type,
|
||||||
|
quick_play_version,
|
||||||
version_info
|
version_info
|
||||||
.logging
|
.logging
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -646,7 +664,8 @@ pub async fn launch_minecraft(
|
|||||||
&version.type_,
|
&version.type_,
|
||||||
*resolution,
|
*resolution,
|
||||||
&java_version.architecture,
|
&java_version.architecture,
|
||||||
quick_play_type,
|
&quick_play_type,
|
||||||
|
quick_play_version,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_iter(),
|
.into_iter(),
|
||||||
|
|||||||
102
packages/app-lib/src/launcher/quick_play_version.rs
Normal file
102
packages/app-lib/src/launcher/quick_play_version.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
use daedalus::minecraft::Version;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// If modified, also update QuickPlayServerVersion.java
|
||||||
|
#[derive(
|
||||||
|
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum QuickPlayServerVersion {
|
||||||
|
Builtin,
|
||||||
|
BuiltinLegacy,
|
||||||
|
Injected,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuickPlayServerVersion {
|
||||||
|
pub fn min_version(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Self::Builtin => Some("23w14a"),
|
||||||
|
Self::BuiltinLegacy => Some("13w17a"),
|
||||||
|
Self::Injected => Some("a1.0.5_01"),
|
||||||
|
Self::Unsupported => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn older_version(&self) -> Option<Self> {
|
||||||
|
match self {
|
||||||
|
Self::Builtin => Some(Self::BuiltinLegacy),
|
||||||
|
Self::BuiltinLegacy => Some(Self::Injected),
|
||||||
|
Self::Injected => Some(Self::Unsupported),
|
||||||
|
Self::Unsupported => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If modified, also update QuickPlaySingleplayerVersion.java
|
||||||
|
#[derive(
|
||||||
|
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum QuickPlaySingleplayerVersion {
|
||||||
|
Builtin,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuickPlaySingleplayerVersion {
|
||||||
|
pub fn min_version(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Self::Builtin => Some("23w14a"),
|
||||||
|
Self::Unsupported => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn older_version(&self) -> Option<Self> {
|
||||||
|
match self {
|
||||||
|
Self::Builtin => Some(Self::Unsupported),
|
||||||
|
Self::Unsupported => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct QuickPlayVersion {
|
||||||
|
pub server: QuickPlayServerVersion,
|
||||||
|
pub singleplayer: QuickPlaySingleplayerVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuickPlayVersion {
|
||||||
|
pub fn find_version(version_index: usize, versions: &[Version]) -> Self {
|
||||||
|
let mut server = QuickPlayServerVersion::Builtin;
|
||||||
|
let mut server_version = server.min_version();
|
||||||
|
|
||||||
|
let mut singleplayer = QuickPlaySingleplayerVersion::Builtin;
|
||||||
|
let mut singleplayer_version = singleplayer.min_version();
|
||||||
|
|
||||||
|
for version in versions.iter().take(version_index - 1) {
|
||||||
|
if let Some(check_version) = server_version
|
||||||
|
&& version.id == check_version
|
||||||
|
{
|
||||||
|
// Safety: older_version will always be Some when min_version is Some
|
||||||
|
server = server.older_version().unwrap();
|
||||||
|
server_version = server.min_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(check_version) = singleplayer_version
|
||||||
|
&& version.id == check_version
|
||||||
|
{
|
||||||
|
singleplayer = singleplayer.older_version().unwrap();
|
||||||
|
singleplayer_version = singleplayer.min_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
if server_version.is_none() && singleplayer_version.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
server,
|
||||||
|
singleplayer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ use crate::event::emit::{emit_process, emit_profile};
|
|||||||
use crate::event::{ProcessPayloadType, ProfilePayloadType};
|
use crate::event::{ProcessPayloadType, ProfilePayloadType};
|
||||||
use crate::profile;
|
use crate::profile;
|
||||||
use crate::util::io::IOError;
|
use crate::util::io::IOError;
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use quick_xml::Reader;
|
use quick_xml::Reader;
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
@ -493,6 +493,16 @@ impl Process {
|
|||||||
if let Err(e) = Self::append_to_log_file(&log_path, &line) {
|
if let Err(e) = Self::append_to_log_file(&log_path, &line) {
|
||||||
tracing::warn!("Failed to write to log file: {}", e);
|
tracing::warn!("Failed to write to log file: {}", e);
|
||||||
}
|
}
|
||||||
|
if let Err(e) = Self::maybe_handle_old_server_join_logging(
|
||||||
|
profile_path,
|
||||||
|
line.trim_ascii_end(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to handle old server join logging: {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
line.clear();
|
line.clear();
|
||||||
@ -540,17 +550,6 @@ impl Process {
|
|||||||
timestamp: &str,
|
timestamp: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let Some(host_port_string) = message.strip_prefix("Connecting to ")
|
|
||||||
else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let Some((host, port_string)) = host_port_string.rsplit_once(", ")
|
|
||||||
else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let Some(port) = port_string.parse::<u16>().ok() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let timestamp = timestamp
|
let timestamp = timestamp
|
||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
.map(|x| x / 1000)
|
.map(|x| x / 1000)
|
||||||
@ -566,6 +565,46 @@ impl Process {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
Self::parse_and_insert_server_join(profile_path, message, timestamp)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn maybe_handle_old_server_join_logging(
|
||||||
|
profile_path: &str,
|
||||||
|
line: &str,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
if let Some((timestamp, message)) = line.split_once(" [CLIENT] [INFO] ")
|
||||||
|
{
|
||||||
|
let timestamp =
|
||||||
|
NaiveDateTime::parse_from_str(timestamp, "%Y-%m-%d %H:%M:%S")?
|
||||||
|
.and_local_timezone(chrono::Local)
|
||||||
|
.map(|x| x.to_utc())
|
||||||
|
.single()
|
||||||
|
.unwrap_or_else(Utc::now);
|
||||||
|
Self::parse_and_insert_server_join(profile_path, message, timestamp)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Self::parse_and_insert_server_join(profile_path, line, Utc::now())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_and_insert_server_join(
|
||||||
|
profile_path: &str,
|
||||||
|
message: &str,
|
||||||
|
timestamp: DateTime<Utc>,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let Some(host_port_string) = message.strip_prefix("Connecting to ")
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some((host, port_string)) = host_port_string.rsplit_once(", ")
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Some(port) = port_string.parse::<u16>().ok() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
let state = crate::State::get().await?;
|
let state = crate::State::get().await?;
|
||||||
crate::state::server_join_log::JoinLogEntry {
|
crate::state::server_join_log::JoinLogEntry {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user