You've already forked AstralRinth
* Begin work on worlds backend * Finish implementing get_profile_worlds and get_server_status (except pinning) * Create TS types and manually copy unparsed chat components * Clippy fix * Update types.d.ts * Initial worlds UI work * Fix api::get_profile_worlds to take in a relative path * sanitize & security update * Fix sanitizePotentialFileUrl * Fix sanitizePotentialFileUrl (for real) * Fix empty motd causing error * Finally actually fix world icons * Fix world icon not being visible on non-Windows * Use the correct generics to take in AppHandle * Implement start_join_singleplayer_world and start_join_server for modern versions * Don't error if server has no cached icon * Migrate to own server pinging * Ignore missing server hidden field and missing saves dir * Update world list frontend * More frontend work * Server status player sample can be absent * Fix refresh state * Add get_profile_protocol_version * Add protocol_version column to database * SQL INTEGER is i64 in sqlx * sqlx prepare * Cache protocol version in database * Continue worlds UI work * Fix motds being bold * Remove legacy pinging and add a 30-second timeout * Remove pinned for now and match world (and server) parsing closer to spec * Move type ServerStatus to worlds.ts * Implement add_server_to_profile * Fix pack_status being ignored when joining from launcher * Make World path field be relative * Implement rename_world and reset_world_icon * Clippy fix * Fix rename_world * UI enhancements * Implement backup_world, which returns the backup size in bytes * Clippy fix * Return index when adding servers to profile * Fix backup * Implement delete_world * Implement edit_server_in_profile and remove_server_from_profile * Clippy fix * Log server joins * Add edit and delete support * Fix ts errors * Fix minecraft font * Switch font out for non-monospaced. * Fix font proper * Some more world cleanup, handle play state, check quickplay compatibility * Clear the cached protocol version when a profile's game version is changed * Fix tint colors in navbar * Fix server protocol version pinging * UI fixes * Fix protocol version handler * Fix MOTD parsing * Add worlds_updated profile event * fix pkg * Functional home screen with worlds * lint * Fix incorrect folder creation * Make items clickable * Add locked field to SingleplayerWorld indicating whether the world is locked by the game * Implement locking frontend * Fix locking condition * Split worlds_updated profile event into servers_updated and world_updated * Fix compile error * Use port from resolve SRV record * Fix serialization of ProfilePayload and ProfilePayloadType * Individual singleplayer world refreshing * Log when worlds are perceived to be updated * Push logging + total refresh lock * Unlisten fixes * Highlight current world when clicked * Launcher logs refactor (#3444) * Switch live log to use STDOUT * fix clippy, legacy logs support * Fix lint * Handle non-XML log messages in XML logging, and don't escape log messages into XML --------- Co-authored-by: Josiah Glosson <soujournme@gmail.com> * Update incompatibility text * Home page fixes, and unlock after close * Remove logging * Add join log database migration * Switch server join timing to being in the database instead of in a separate log file * Create optimized get_recent_worlds function that takes in a limit * Update dependencies and fix Cargo.lock * temp disable overflow menus * revert home page changes * Enable overflow menus again * Remove list * Revert * Push dev tools * Remove default filter * Disable debug renderer * Fix random app errors * Refactor * Fix missing computed import * Fix light mode issues * Fix TS errors * Lint * Fix bad link in change modpack version modal * fix lint * fix intl --------- Co-authored-by: Josiah Glosson <soujournme@gmail.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
304 lines
8.1 KiB
TypeScript
304 lines
8.1 KiB
TypeScript
import { invoke } from '@tauri-apps/api/core'
|
|
import { get_full_path } from '@/helpers/profile'
|
|
import { openPath } from '@/helpers/utils'
|
|
import { autoToHTML } from '@geometrically/minecraft-motd-parser'
|
|
import dayjs from 'dayjs'
|
|
import type { GameVersion } from '@modrinth/ui'
|
|
|
|
type BaseWorld = {
|
|
name: string
|
|
last_played?: string
|
|
icon?: string
|
|
}
|
|
|
|
export type SingleplayerWorld = BaseWorld & {
|
|
type: 'singleplayer'
|
|
path: string
|
|
game_mode: SingleplayerGameMode
|
|
hardcore: boolean
|
|
locked: boolean
|
|
}
|
|
|
|
export type ServerWorld = BaseWorld & {
|
|
type: 'server'
|
|
index: number
|
|
address: string
|
|
pack_status: ServerPackStatus
|
|
}
|
|
|
|
export type World = SingleplayerWorld | ServerWorld
|
|
|
|
export type WorldWithProfile = {
|
|
profile: string
|
|
} & World
|
|
|
|
export type SingleplayerGameMode = 'survival' | 'creative' | 'adventure' | 'spectator'
|
|
export type ServerPackStatus = 'enabled' | 'disabled' | 'prompt'
|
|
|
|
export type ServerStatus = {
|
|
// https://minecraft.wiki/w/Text_component_format
|
|
description?: string | Chat
|
|
players?: {
|
|
max: number
|
|
online: number
|
|
sample: { name: string; id: string }[]
|
|
}
|
|
version?: {
|
|
name: string
|
|
protocol: number
|
|
}
|
|
favicon?: string
|
|
enforces_secure_chat: boolean
|
|
ping?: number
|
|
}
|
|
|
|
export interface Chat {
|
|
text: string
|
|
bold: boolean
|
|
italic: boolean
|
|
underlined: boolean
|
|
strikethrough: boolean
|
|
obfuscated: boolean
|
|
color?: string
|
|
extra: Chat[]
|
|
}
|
|
|
|
export type ServerData = {
|
|
refreshing: boolean
|
|
status?: ServerStatus
|
|
rawMotd?: string | Chat
|
|
renderedMotd?: string
|
|
}
|
|
|
|
export async function get_recent_worlds(limit: number): Promise<WorldWithProfile[]> {
|
|
return await invoke('plugin:worlds|get_recent_worlds', { limit })
|
|
}
|
|
|
|
export async function get_profile_worlds(path: string): Promise<World[]> {
|
|
return await invoke('plugin:worlds|get_profile_worlds', { path })
|
|
}
|
|
|
|
export async function get_singleplayer_world(
|
|
instance: string,
|
|
world: string,
|
|
): Promise<SingleplayerWorld> {
|
|
return await invoke('plugin:worlds|get_singleplayer_world', { instance, world })
|
|
}
|
|
|
|
export async function rename_world(
|
|
instance: string,
|
|
world: string,
|
|
newName: string,
|
|
): Promise<void> {
|
|
return await invoke('plugin:worlds|rename_world', { instance, world, newName })
|
|
}
|
|
|
|
export async function reset_world_icon(instance: string, world: string): Promise<void> {
|
|
return await invoke('plugin:worlds|reset_world_icon', { instance, world })
|
|
}
|
|
|
|
export async function backup_world(instance: string, world: string): Promise<number> {
|
|
return await invoke('plugin:worlds|backup_world', { instance, world })
|
|
}
|
|
|
|
export async function delete_world(instance: string, world: string): Promise<void> {
|
|
return await invoke('plugin:worlds|delete_world', { instance, world })
|
|
}
|
|
|
|
export async function add_server_to_profile(
|
|
path: string,
|
|
name: string,
|
|
address: string,
|
|
packStatus: ServerPackStatus,
|
|
): Promise<number> {
|
|
return await invoke('plugin:worlds|add_server_to_profile', { path, name, address, packStatus })
|
|
}
|
|
|
|
export async function edit_server_in_profile(
|
|
path: string,
|
|
index: number,
|
|
name: string,
|
|
address: string,
|
|
packStatus: ServerPackStatus,
|
|
): Promise<void> {
|
|
return await invoke('plugin:worlds|edit_server_in_profile', {
|
|
path,
|
|
index,
|
|
name,
|
|
address,
|
|
packStatus,
|
|
})
|
|
}
|
|
|
|
export async function remove_server_from_profile(path: string, index: number): Promise<void> {
|
|
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
|
}
|
|
|
|
export async function get_profile_protocol_version(path: string): Promise<number | null> {
|
|
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
|
}
|
|
|
|
export async function get_server_status(
|
|
address: string,
|
|
protocolVersion: number | null = null,
|
|
): Promise<ServerStatus> {
|
|
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
|
}
|
|
|
|
export async function start_join_singleplayer_world(path: string, world: string): Promise<unknown> {
|
|
return await invoke('plugin:worlds|start_join_singleplayer_world', { path, world })
|
|
}
|
|
|
|
export async function start_join_server(path: string, address: string): Promise<unknown> {
|
|
return await invoke('plugin:worlds|start_join_server', { path, address })
|
|
}
|
|
|
|
export async function showWorldInFolder(instancePath: string, worldPath: string) {
|
|
const fullPath = await get_full_path(instancePath)
|
|
return await openPath(fullPath + '/saves/' + worldPath)
|
|
}
|
|
|
|
export function getWorldIdentifier(world: World) {
|
|
return world.type === 'singleplayer' ? world.path : world.address
|
|
}
|
|
|
|
export function sortWorlds(worlds: World[]) {
|
|
worlds.sort((a, b) => {
|
|
if (!a.last_played) {
|
|
return 1
|
|
}
|
|
if (!b.last_played) {
|
|
return -1
|
|
}
|
|
return dayjs(b.last_played).diff(dayjs(a.last_played))
|
|
})
|
|
}
|
|
|
|
export function isSingleplayerWorld(world: World): world is SingleplayerWorld {
|
|
return world.type === 'singleplayer'
|
|
}
|
|
|
|
export function isServerWorld(world: World): world is ServerWorld {
|
|
return world.type === 'server'
|
|
}
|
|
|
|
export async function refreshServerData(
|
|
serverData: ServerData,
|
|
protocolVersion: number | null,
|
|
address: string,
|
|
): Promise<void> {
|
|
serverData.refreshing = true
|
|
await get_server_status(address, protocolVersion)
|
|
.then((status) => {
|
|
serverData.status = status
|
|
if (status.description) {
|
|
serverData.rawMotd = status.description
|
|
serverData.renderedMotd = autoToHTML(status.description)
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error(`Refreshing addr: ${address}`, err)
|
|
})
|
|
.finally(() => {
|
|
serverData.refreshing = false
|
|
})
|
|
}
|
|
|
|
export async function refreshServers(
|
|
worlds: World[],
|
|
serverData: Record<string, ServerData>,
|
|
protocolVersion: number | null,
|
|
) {
|
|
const servers = worlds.filter(isServerWorld)
|
|
servers.forEach((server) => {
|
|
if (!serverData[server.address]) {
|
|
serverData[server.address] = {
|
|
refreshing: true,
|
|
}
|
|
} else {
|
|
serverData[server.address].refreshing = true
|
|
}
|
|
})
|
|
|
|
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
|
Promise.all(
|
|
Object.keys(serverData).map((address) =>
|
|
refreshServerData(serverData[address], protocolVersion, address),
|
|
),
|
|
)
|
|
}
|
|
|
|
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
|
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
|
if (index !== -1) {
|
|
worlds[index] = await get_singleplayer_world(instancePath, worldPath)
|
|
sortWorlds(worlds)
|
|
} else {
|
|
console.error(`Error refreshing world, could not find world at path ${worldPath}.`)
|
|
}
|
|
}
|
|
|
|
export async function handleDefaultProfileUpdateEvent(
|
|
worlds: World[],
|
|
instancePath: string,
|
|
e: ProfileEvent,
|
|
) {
|
|
if (e.event === 'world_updated') {
|
|
await refreshWorld(worlds, instancePath, e.world)
|
|
}
|
|
|
|
if (e.event === 'server_joined') {
|
|
const world = worlds.find(
|
|
(w) =>
|
|
w.type === 'server' &&
|
|
(w.address === `${e.host}:${e.port}` || (e.port == 25565 && w.address == e.host)),
|
|
)
|
|
if (world) {
|
|
world.last_played = e.timestamp
|
|
sortWorlds(worlds)
|
|
} else {
|
|
console.error(`Could not find world for server join event: ${e.host}:${e.port}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function refreshWorlds(instancePath: string): Promise<World[]> {
|
|
const worlds = await get_profile_worlds(instancePath).catch((err) => {
|
|
console.error(`Error refreshing worlds for instance: ${instancePath}`, err)
|
|
})
|
|
if (worlds) {
|
|
sortWorlds(worlds)
|
|
}
|
|
|
|
return worlds ?? []
|
|
}
|
|
|
|
const FIRST_QUICK_PLAY_VERSION = '23w14a'
|
|
|
|
export function hasQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
|
if (!gameVersions.length) {
|
|
return false
|
|
}
|
|
|
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
|
const targetIndex = gameVersions.findIndex((v) => v.version === FIRST_QUICK_PLAY_VERSION)
|
|
|
|
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
|
}
|
|
|
|
export type ProfileEvent = { profile_path_id: string } & (
|
|
| {
|
|
event: 'servers_updated'
|
|
}
|
|
| {
|
|
event: 'world_updated'
|
|
world: string
|
|
}
|
|
| {
|
|
event: 'server_joined'
|
|
host: string
|
|
port: number
|
|
timestamp: string
|
|
}
|
|
)
|