Compare commits

..

No commits in common. "main" and "prod" have entirely different histories.
main ... prod

31 changed files with 189 additions and 802 deletions

1
.idea/code.iml generated
View File

@ -10,6 +10,7 @@
<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>

View File

@ -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/**/*.{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/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",
"test": "vue-tsc --noEmit" "test": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {

View File

@ -21,11 +21,14 @@ const props = defineProps({
}) })
const featuredCategory = computed(() => { const featuredCategory = computed(() => {
if (props.project.display_categories.includes('optimization')) { if (props.project.categories.includes('optimization')) {
return 'optimization' return 'optimization'
} }
return props.project.display_categories[0] ?? props.project.categories[0] if (props.project.categories.length > 0) {
return props.project.categories[0]
}
return undefined
}) })
const toColor = computed(() => { const toColor = computed(() => {

View File

@ -76,10 +76,10 @@ const installing = ref(false)
const onInstall = ref(() => {}) const onInstall = ref(() => {})
defineExpose({ defineExpose({
show: (instanceVal, projectVal, projectVersions, selected, callback) => { show: (instanceVal, projectVal, projectVersions, callback) => {
instance.value = instanceVal instance.value = instanceVal
versions.value = projectVersions versions.value = projectVersions
selectedVersion.value = selected ?? projectVersions[0] selectedVersion.value = projectVersions[0]
project.value = projectVal project.value = projectVal

View File

@ -6,8 +6,9 @@ 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,
@ -60,8 +61,7 @@ const props = withDefaults(
playingInstance?: boolean playingInstance?: boolean
playingWorld?: boolean playingWorld?: boolean
startingInstance?: boolean startingInstance?: boolean
supportsServerQuickPlay?: boolean supportsQuickPlay?: boolean
supportsWorldQuickPlay?: boolean
currentProtocol?: ProtocolVersion | null currentProtocol?: ProtocolVersion | null
highlighted?: boolean highlighted?: boolean
@ -85,8 +85,7 @@ const props = withDefaults(
playingInstance: false, playingInstance: false,
playingWorld: false, playingWorld: false,
startingInstance: false, startingInstance: false,
supportsServerQuickPlay: true, supportsQuickPlay: false,
supportsWorldQuickPlay: false,
currentProtocol: null, currentProtocol: null,
refreshing: false, refreshing: false,
@ -129,13 +128,9 @@ const messages = defineMessages({
id: 'instance.worlds.a_minecraft_server', id: 'instance.worlds.a_minecraft_server',
defaultMessage: 'A Minecraft Server', defaultMessage: 'A Minecraft Server',
}, },
noServerQuickPlay: { noQuickPlay: {
id: 'instance.worlds.no_server_quick_play', id: 'instance.worlds.no_quick_play',
defaultMessage: 'You can only jump straight into servers on Minecraft Alpha 1.0.5+', defaultMessage: 'You can only jump straight into worlds on Minecraft 1.20+',
},
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',
@ -157,6 +152,10 @@ 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',
@ -331,24 +330,17 @@ const messages = defineMessages({
<ButtonStyled v-else> <ButtonStyled v-else>
<button <button
v-tooltip=" v-tooltip="
world.type == 'server' && !supportsServerQuickPlay !serverStatus
? formatMessage(messages.noServerQuickPlay) ? formatMessage(messages.noContact)
: world.type == 'singleplayer' && !supportsWorldQuickPlay : serverIncompatible
? formatMessage(messages.noSingleplayerQuickPlay) ? formatMessage(messages.incompatibleServer)
: playingOtherWorld || locked : !supportsQuickPlay
? formatMessage(messages.gameAlreadyOpen) ? formatMessage(messages.noQuickPlay)
: !serverStatus : playingOtherWorld || locked
? formatMessage(messages.noContact) ? formatMessage(messages.gameAlreadyOpen)
: serverIncompatible : null
? formatMessage(messages.incompatibleServer)
: null
"
:disabled="
playingOtherWorld ||
startingInstance ||
(world.type == 'server' && !supportsServerQuickPlay) ||
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
" "
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
@click="emit('play')" @click="emit('play')"
> >
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" /> <SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
@ -365,6 +357,11 @@ 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,
@ -430,6 +427,10 @@ 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) }}

View File

@ -311,24 +311,15 @@ export async function refreshWorlds(instancePath: string): Promise<World[]> {
return worlds ?? [] return worlds ?? []
} }
export function hasServerQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) { const FIRST_QUICK_PLAY_VERSION = '23w14a'
if (!gameVersions.length) {
return true
}
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion) export function hasQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
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 === '23w14a') const targetIndex = gameVersions.findIndex((v) => v.version === FIRST_QUICK_PLAY_VERSION)
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
} }

View File

@ -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_server_quick_play": { "instance.worlds.no_quick_play": {
"message": "You can only jump straight into servers on Minecraft Alpha 1.0.5+" "message": "You can only jump straight into worlds on Minecraft 1.20+"
}, },
"instance.worlds.no_singleplayer_quick_play": { "instance.worlds.play_anyway": {
"message": "You can only jump straight into singleplayer worlds on Minecraft 1.20+" "message": "Play anyway"
}, },
"instance.worlds.play_instance": { "instance.worlds.play_instance": {
"message": "Play instance" "message": "Play instance"

View File

@ -67,8 +67,7 @@
: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-server-quick-play="supportsServerQuickPlay" :supports-quick-play="supportsQuickPlay"
: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)"
@ -151,11 +150,10 @@ import {
refreshWorld, refreshWorld,
sortWorlds, sortWorlds,
refreshServers, refreshServers,
hasWorldQuickPlaySupport, hasQuickPlaySupport,
refreshWorlds, refreshWorlds,
handleDefaultProfileUpdateEvent, handleDefaultProfileUpdateEvent,
showWorldInFolder, showWorldInFolder,
hasServerQuickPlaySupport,
} from '@/helpers/worlds.ts' } from '@/helpers/worlds.ts'
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue' import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue' import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
@ -357,11 +355,8 @@ 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 supportsServerQuickPlay = computed(() => const supportsQuickPlay = computed(() =>
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version), hasQuickPlaySupport(gameVersions.value, instance.value.game_version),
)
const supportsWorldQuickPlay = computed(() =>
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
) )
const filterOptions = computed(() => { const filterOptions = computed(() => {

View File

@ -29,8 +29,8 @@ export const useInstall = defineStore('installStore', {
setIncompatibilityWarningModal(ref) { setIncompatibilityWarningModal(ref) {
this.incompatibilityWarningModal = ref this.incompatibilityWarningModal = ref
}, },
showIncompatibilityWarningModal(instance, project, versions, selected, onInstall) { showIncompatibilityWarningModal(instance, project, versions, onInstall) {
this.incompatibilityWarningModal.show(instance, project, versions, selected, onInstall) this.incompatibilityWarningModal.show(instance, project, versions, onInstall)
}, },
setModInstallModal(ref) { setModInstallModal(ref) {
this.modInstallModal = ref this.modInstallModal = ref
@ -133,13 +133,7 @@ export const install = async (
callback(version.id) callback(version.id)
} else { } else {
const install = useInstall() const install = useInstall()
install.showIncompatibilityWarningModal( install.showIncompatibilityWarningModal(instance, project, projectVersions, callback)
instance,
project,
projectVersions,
version,
callback,
)
} }
} else { } else {
const versions = (await get_version_many(project.versions).catch(handleError)).sort( const versions = (await get_version_many(project.versions).catch(handleError)).sort(

View File

@ -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)
} }

View File

@ -4,7 +4,6 @@ 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,
@ -204,7 +203,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)
} }
@ -214,11 +213,8 @@ pub async fn start_join_server(
path: &str, path: &str,
address: &str, address: &str,
) -> Result<ProcessMetadata> { ) -> Result<ProcessMetadata> {
let process = profile::run( let process =
path, profile::run(path, &QuickPlayType::Server(address.to_owned())).await?;
QuickPlayType::Server(ServerAddress::Unresolved(address.to_owned())),
)
.await?;
Ok(process) Ok(process)
} }

View File

@ -143,13 +143,8 @@ export default defineNuxtConfig({
state.lastGenerated && state.lastGenerated &&
new Date(state.lastGenerated).getTime() + TTL > new Date().getTime() && new Date(state.lastGenerated).getTime() + TTL > new Date().getTime() &&
// ...but only if the API URL is the same // ...but only if the API URL is the same
state.apiUrl === API_URL && state.apiUrl === API_URL
// ...and if no errors were caught during the last generation
(state.errors ?? []).length === 0
) { ) {
console.log(
"Tags already recently generated. Delete apps/frontend/generated/state.json to force regeneration.",
);
return; return;
} }

View File

@ -2,10 +2,7 @@
<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 <div v-for="link in navLinks" :key="link.label">
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"
@ -43,7 +40,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; shown?: boolean }[]; navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
route: RouteLocationNormalized; route: RouteLocationNormalized;
server: ModrinthServer; server: ModrinthServer;
backupInProgress?: BackupInProgressReason; backupInProgress?: BackupInProgressReason;

View File

@ -6,7 +6,6 @@ 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;

View File

@ -16,15 +16,12 @@ 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;
@ -35,11 +32,7 @@ useHead({
title: `Options - ${props.server.general?.name ?? "Server"} - Modrinth`, title: `Options - ${props.server.general?.name ?? "Server"} - Modrinth`,
}); });
const ownerId = computed(() => props.server.general?.owner_id ?? "Ghost"); const navLinks = [
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` },
@ -55,15 +48,7 @@ const navLinks = computed(() => [
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>

View File

@ -53,7 +53,6 @@ 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");

View File

@ -1,7 +1,6 @@
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 {
@ -9,9 +8,6 @@ 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")
} }
@ -35,17 +31,7 @@ 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") {

View File

@ -1,45 +0,0 @@
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);
};
}
}

View File

@ -1,12 +0,0 @@
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"));
}

View File

@ -1,85 +0,0 @@
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;
});
}
}

View File

@ -1,20 +0,0 @@
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;
}
}

View File

@ -1,14 +0,0 @@
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;
}
}

View File

@ -1,99 +0,0 @@
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;
}
}

View File

@ -11,7 +11,6 @@ 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;

View File

@ -23,7 +23,6 @@ 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::{
@ -41,7 +40,7 @@ pub mod update;
pub enum QuickPlayType { pub enum QuickPlayType {
None, None,
Singleplayer(String), Singleplayer(String),
Server(ServerAddress), Server(String),
} }
/// Remove a profile /// Remove a profile
@ -631,7 +630,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?;
@ -647,7 +646,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?;

View File

@ -1,166 +0,0 @@
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)),
)
}

View File

@ -1,7 +1,6 @@
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,
@ -12,7 +11,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::{ErrorKind, Result, State, launcher}; use crate::{Error, 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};
@ -25,9 +24,11 @@ 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;
@ -432,9 +433,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)
{ {
// We can't check for the profile already having a last_played, in case the user joined if world.last_played.is_some() {
// the target address directly more recently. This is often the case when using continue;
// 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)
{ {
@ -916,3 +917,93 @@ 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)),
)
}

View File

@ -1,6 +1,5 @@
//! Minecraft CLI argument logic //! Minecraft CLI argument logic
use crate::launcher::quick_play_version::QuickPlayServerVersion; use crate::launcher::parse_rules;
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::{
@ -116,13 +115,11 @@ 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();
@ -158,45 +155,13 @@ 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);
@ -260,13 +225,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;
let mut parsed_arguments = Vec::new();
if let Some(arguments) = arguments { if let Some(arguments) = arguments {
let mut parsed_arguments = Vec::new();
parse_arguments( parse_arguments(
arguments, arguments,
&mut parsed_arguments, &mut parsed_arguments,
@ -288,7 +253,10 @@ 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),
@ -304,21 +272,10 @@ 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)]
@ -397,9 +354,9 @@ fn parse_minecraft_argument(
) )
.replace( .replace(
"${quickPlayMultiplayer}", "${quickPlayMultiplayer}",
&match quick_play_type { match quick_play_type {
QuickPlayType::Server(address) => address.to_string(), QuickPlayType::Server(address) => address,
_ => "".to_string(), _ => "",
}, },
)) ))
} }

View File

@ -4,9 +4,6 @@ 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,
@ -28,7 +25,6 @@ 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
@ -461,7 +457,7 @@ pub async fn launch_minecraft(
credentials: &Credentials, credentials: &Credentials,
post_exit_hook: Option<String>, post_exit_hook: Option<String>,
profile: &Profile, profile: &Profile,
mut quick_play_type: QuickPlayType, 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
@ -593,18 +589,6 @@ 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")?;
@ -622,13 +606,11 @@ 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()
@ -664,8 +646,7 @@ 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(),

View File

@ -1,102 +0,0 @@
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,
}
}
}

View File

@ -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, NaiveDateTime, TimeZone, Utc}; use chrono::{DateTime, 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,16 +493,6 @@ 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();
@ -550,6 +540,17 @@ 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)
@ -565,46 +566,6 @@ 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 {