Skip to content

Commit 31a6579

Browse files
committed
local server support! finish all
1 parent f4632c5 commit 31a6579

10 files changed

+220
-63
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"esbuild-plugin-polyfill-node": "^0.3.0",
7676
"express": "^4.18.2",
7777
"filesize": "^10.0.12",
78-
"flying-squid": "npm:@zardoy/flying-squid@^0.0.51",
78+
"flying-squid": "npm:@zardoy/flying-squid@^0.0.57",
7979
"fs-extra": "^11.1.1",
8080
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
8181
"jszip": "^3.10.1",

pnpm-lock.yaml

+6-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/clientMods.ts

+53-14
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const protectRuntime = () => {
1313
if (sillyProtection) return
1414
sillyProtection = true
1515
const sensetiveKeys = new Set(['authenticatedAccounts', 'serversList', 'username'])
16-
window.localStorage = new Proxy(window.localStorage, {
16+
const proxy = new Proxy(window.localStorage, {
1717
get (target, prop) {
1818
if (typeof prop === 'string') {
1919
if (sensetiveKeys.has(prop)) {
@@ -69,6 +69,11 @@ const protectRuntime = () => {
6969
return Reflect.deleteProperty(target, prop)
7070
}
7171
})
72+
Object.defineProperty(window, 'localStorage', {
73+
value: proxy,
74+
writable: false,
75+
configurable: false,
76+
})
7277
}
7378

7479
// #region Database
@@ -198,6 +203,7 @@ window.mcraft = {
198203
}
199204

200205
const activateMod = async (mod: ClientMod, reason: string) => {
206+
if (mod.enabled === false) return false
201207
protectRuntime()
202208
console.debug(`Activating mod ${mod.name} (${reason})...`)
203209
window.loadedMods ??= {}
@@ -280,24 +286,35 @@ const installOrUpdateMod = async (repo: Repository, mod: ClientModDefinition, ac
280286
}
281287
if (mod.stylesGlobal) {
282288
await progress?.executeWithMessage(
283-
`Installing ${mod.name} styles`,
289+
`Downloading ${mod.name} styles`,
284290
async () => {
285291
mod.stylesGlobal = await fetchData(['global.css']) as any
286292
}
287293
)
288294
}
289295
if (mod.scriptMainUnstable) {
290296
await progress?.executeWithMessage(
291-
`Installing ${mod.name} script`,
297+
`Downloading ${mod.name} script`,
292298
async () => {
293299
mod.scriptMainUnstable = await fetchData(['mainUnstable.js']) as any
294300
}
295301
)
296302
}
303+
if (mod.serverPlugin) {
304+
if (mod.name.endsWith('.disabled')) throw new Error(`Mod name ${mod.name} can't end with .disabled`)
305+
await progress?.executeWithMessage(
306+
`Downloading ${mod.name} server plugin`,
307+
async () => {
308+
mod.serverPlugin = await fetchData(['serverPlugin.js']) as any
309+
}
310+
)
311+
}
297312
if (activate) {
298-
const result = await activateMod(mod as ClientMod, 'install')
299-
if (!result) {
313+
// todo try to de-activate mod if it's already loaded
314+
if (window.loadedMods?.[mod.name]) {
300315
modsWaitingReloadStatus[mod.name] = true
316+
} else {
317+
await activateMod(mod as ClientMod, 'install')
301318
}
302319
}
303320
await savePlugin(mod as ClientMod)
@@ -324,7 +341,7 @@ const checkRepositoryUpdates = async (repo: Repository) => {
324341

325342
}
326343

327-
const fetchRepository = async (urlOriginal: string, url: string, hasMirrors = false) => {
344+
export const fetchRepository = async (urlOriginal: string, url: string, hasMirrors = false) => {
328345
const fetchUrl = normalizeRepoUrl(url).replace(/\/$/, '') + '/mcraft-repo.json'
329346
try {
330347
const response = await fetch(fetchUrl).then(async res => res.json())
@@ -393,18 +410,21 @@ export const uninstallModAction = async (name: string) => {
393410
delete modsErrors[name]
394411
}
395412

396-
export const setEnabledModAction = async (name: string, enabled: boolean) => {
413+
export const setEnabledModAction = async (name: string, newEnabled: boolean) => {
397414
const mod = await getPlugin(name)
398415
if (!mod) throw new Error(`Mod ${name} not found`)
399-
if (enabled) {
400-
if (window.loadedMods?.[mod.name]) {
401-
mod.enabled = true
402-
} else {
416+
if (newEnabled) {
417+
mod.enabled = true
418+
if (!window.loadedMods?.[mod.name]) {
403419
await activateMod(mod, 'manual')
404420
}
405421
} else {
406422
// todo deactivate mod
407423
mod.enabled = false
424+
if (window.loadedMods?.[mod.name]?.deactivate) {
425+
window.loadedMods[mod.name].deactivate()
426+
delete window.loadedMods[mod.name]
427+
}
408428
}
409429
await savePlugin(mod)
410430
}
@@ -420,7 +440,9 @@ export const getAllModsDisplayList = async () => {
420440
const mapMods = (mapMods: ClientMod[]) => mapMods.map(mod => ({
421441
...mod,
422442
installed: installedMods.some(m => m.name === mod.name),
423-
enabled: !!window.loadedMods?.[mod.name]
443+
activated: !!window.loadedMods?.[mod.name],
444+
installedVersion: installedMods.find(m => m.name === mod.name)?.version,
445+
canBeActivated: mod.scriptMainUnstable || mod.stylesGlobal,
424446
}))
425447
return {
426448
repos: repos.map(repo => ({
@@ -433,7 +455,7 @@ export const getAllModsDisplayList = async () => {
433455

434456
export const removeRepositoryAction = async (url: string) => {
435457
// todo remove mods
436-
const choice = await showOptionsModal('Remove repository? Installed mods won\' be automatically removed.', ['Yes'])
458+
const choice = await showOptionsModal('Remove repository? Installed mods wont be automatically removed.', ['Yes'])
437459
if (!choice) return
438460
await deleteRepository(url)
439461
modsReactiveUpdater.counter++
@@ -458,4 +480,21 @@ export const addRepositoryAction = async () => {
458480
await fetchRepository(url, url)
459481
}
460482

461-
// export const getAllMods = () => {}
483+
export const getServerPlugin = async (plugin: string) => {
484+
const mod = await getPlugin(plugin)
485+
if (!mod) return null
486+
if (mod.serverPlugin) {
487+
return {
488+
content: mod.serverPlugin,
489+
version: mod.version
490+
}
491+
}
492+
return null
493+
}
494+
495+
export const getAvailableServerPlugins = async () => {
496+
const mods = await getAllMods()
497+
return mods.filter(mod => mod.serverPlugin)
498+
}
499+
500+
window.inspectInstalledMods = getAllMods

src/defaultLocalServerOptions.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
'gameMode': 0,
99
'difficulty': 0,
1010
'worldFolder': 'world',
11+
'pluginsFolder': true,
1112
// todo set sid, disable entities auto-spawn
1213
'generation': {
1314
// grass_field

src/react/CreateWorld.tsx

+43-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { useEffect, useState } from 'react'
22
import { proxy, useSnapshot } from 'valtio'
33
import { filesize } from 'filesize'
4+
import { getAvailableServerPlugins } from '../clientMods'
5+
import { showModal } from '../globalState'
46
import Input from './Input'
57
import Screen from './Screen'
68
import Button from './Button'
79
import SelectGameVersion from './SelectGameVersion'
810
import styles from './createWorld.module.css'
11+
import { InputOption, showInputsModal, showOptionsModal } from './SelectOption'
912

1013
// const worldTypes = ['default', 'flat', 'largeBiomes', 'amplified', 'customized', 'buffet', 'debug_all_block_states']
1114
const worldTypes = ['default', 'flat'/* , 'void' */]
@@ -15,13 +18,14 @@ export const creatingWorldState = proxy({
1518
title: '',
1619
type: worldTypes[0],
1720
gameMode: gameModes[0],
18-
version: ''
21+
version: '',
22+
plugins: [] as string[]
1923
})
2024

2125
export default ({ cancelClick, createClick, customizeClick, versions, defaultVersion }) => {
2226
const [quota, setQuota] = useState('')
2327

24-
const { title, type, version, gameMode } = useSnapshot(creatingWorldState)
28+
const { title, type, version, gameMode, plugins } = useSnapshot(creatingWorldState)
2529
useEffect(() => {
2630
creatingWorldState.version = defaultVersion
2731
void navigator.storage?.estimate?.().then(({ quota, usage }) => {
@@ -69,7 +73,38 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
6973
creatingWorldState.gameMode = gameModes[index === gameModes.length - 1 ? 0 : index + 1]
7074
}}
7175
>
72-
Gamemode: {gameMode}
76+
Game Mode: {gameMode}
77+
</Button>
78+
</div>
79+
<div style={{ display: 'flex' }}>
80+
<Button onClick={async () => {
81+
const availableServerPlugins = await getAvailableServerPlugins()
82+
const availableModNames = availableServerPlugins.map(mod => mod.name)
83+
const choices: Record<string, InputOption> = Object.fromEntries(availableServerPlugins.map(mod => [mod.name, {
84+
type: 'checkbox' as const,
85+
defaultValue: creatingWorldState.plugins.includes(mod.name),
86+
label: mod.name
87+
}]))
88+
choices.installMore = {
89+
type: 'button' as const,
90+
onButtonClick () {
91+
showModal({ reactType: 'mods' })
92+
}
93+
}
94+
const choice = await showInputsModal('Select server plugins from mods to install:', choices)
95+
if (!choice) return
96+
creatingWorldState.plugins = availableModNames.filter(modName => choice[modName])
97+
}}
98+
>Use Mods ({plugins.length})
99+
</Button>
100+
<Button
101+
onClick={() => {
102+
const index = gameModes.indexOf(gameMode)
103+
creatingWorldState.gameMode = gameModes[index === gameModes.length - 1 ? 0 : index + 1]
104+
}}
105+
disabled
106+
>
107+
Save Type: Java
73108
</Button>
74109
</div>
75110
<div className='muted' style={{ fontSize: 8 }}>Default and other world types are WIP</div>
@@ -80,7 +115,11 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
80115
}}
81116
>Cancel
82117
</Button>
83-
<Button disabled={!title} onClick={createClick}>Create</Button>
118+
<Button disabled={!title} onClick={createClick}>
119+
<b>
120+
Create
121+
</b>
122+
</Button>
84123
</div>
85124
<div className='muted' style={{ fontSize: 9 }}>Note: save important worlds in folders on your hard drive!</div>
86125
<div className='muted' style={{ fontSize: 9 }}>{quota}</div>

src/react/CreateWorldProvider.tsx

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import fs from 'fs'
2+
import path from 'path'
13
import { hideCurrentModal, showModal } from '../globalState'
24
import defaultLocalServerOptions from '../defaultLocalServerOptions'
35
import { mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'
46
import supportedVersions from '../supportedVersions.mjs'
7+
import { getServerPlugin } from '../clientMods'
58
import CreateWorld, { WorldCustomize, creatingWorldState } from './CreateWorld'
69
import { getWorldsPath } from './SingleplayerProvider'
710
import { useIsModalActive } from './utilsApp'
@@ -14,7 +17,7 @@ export default () => {
1417
const versions = Object.values(versionsPerMinor).map(x => {
1518
return {
1619
version: x,
17-
label: x === defaultLocalServerOptions.version ? `${x} (available offline)` : x
20+
label: x === defaultLocalServerOptions.version ? `${x} (default)` : x
1821
}
1922
})
2023
return <CreateWorld
@@ -24,10 +27,20 @@ export default () => {
2427
}}
2528
createClick={async () => {
2629
// create new world
27-
const { title, type, version, gameMode } = creatingWorldState
30+
const { title, type, version, gameMode, plugins } = creatingWorldState
2831
// todo display path in ui + disable if exist
2932
const savePath = await uniqueFileNameFromWorldName(title, getWorldsPath())
3033
await mkdirRecursive(savePath)
34+
for (const plugin of plugins) {
35+
// eslint-disable-next-line no-await-in-loop
36+
const { content, version } = await getServerPlugin(plugin) ?? {}
37+
if (content) {
38+
// eslint-disable-next-line no-await-in-loop
39+
await mkdirRecursive(path.join(savePath, 'plugins'))
40+
// eslint-disable-next-line no-await-in-loop
41+
await fs.promises.writeFile(path.join(savePath, 'plugins', `${plugin}-${version}.js`), content)
42+
}
43+
}
3144
let generation
3245
if (type === 'flat') {
3346
generation = {

0 commit comments

Comments
 (0)