mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[www] optional downgrade
This commit is contained in:
+56
-15
@@ -225,21 +225,31 @@ async function uploadRoomBlob(file: File): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file that actually gets stored for a picked scene file. A `.binpb` is a scene taken
|
* The scene files the upload form takes, by extension: a `.room` is a save as this server
|
||||||
* from a newer build, which the build this server runs can't parse as it stands, so it is
|
* stores it, a `.binpb` a scene taken from a newer build. Only checked in the browser —
|
||||||
* downgraded first (see `room-converter.ts`); anything else passes through untouched.
|
* the blob is opaque to the server, which stores whatever it is given.
|
||||||
|
*/
|
||||||
|
const ROOM_BLOB_EXTENSIONS = ['.room', '.binpb']
|
||||||
|
|
||||||
|
function isRoomBlobFile(filename: string): boolean {
|
||||||
|
const name = filename.toLowerCase()
|
||||||
|
return ROOM_BLOB_EXTENSIONS.some((ext) => name.endsWith(ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file that actually gets stored for a picked scene file. A scene taken from a newer
|
||||||
|
* build can't be parsed by the build this server runs as it stands, so when the owner asks
|
||||||
|
* for it the file is downgraded first (see `room-converter.ts`); otherwise it passes
|
||||||
|
* through untouched.
|
||||||
*
|
*
|
||||||
* Done here in the browser, before either request, because the upload goes straight to
|
* Done here in the browser, before either request, because the upload goes straight to
|
||||||
* `storage` and the save's `Hash` has to describe the bytes that were stored — so both
|
* `storage` and the save's `Hash` has to describe the bytes that were stored — so both
|
||||||
* the upload and `blobHash` must be given this file, not the one that was picked.
|
* the upload and `blobHash` must be given this file, not the one that was picked.
|
||||||
*/
|
*/
|
||||||
async function prepareRoomBlob(file: File): Promise<{ file: File; converted: boolean }> {
|
async function prepareRoomBlob(file: File, downgrade: boolean): Promise<File> {
|
||||||
if (!needsDowngrade(file.name)) return { file, converted: false }
|
if (!downgrade) return file
|
||||||
const { data } = downgradeRoom(new Uint8Array(await file.arrayBuffer()))
|
const { data } = downgradeRoom(new Uint8Array(await file.arrayBuffer()))
|
||||||
return {
|
return new File([data], file.name, { type: 'application/octet-stream' })
|
||||||
file: new File([data], file.name, { type: 'application/octet-stream' }),
|
|
||||||
converted: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1877,6 +1887,11 @@ function BlobUpload({
|
|||||||
const [file, setFile] = useState<File | null>(null)
|
const [file, setFile] = useState<File | null>(null)
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [publish, setPublish] = useState(true)
|
const [publish, setPublish] = useState(true)
|
||||||
|
// Whether to convert the file for this build before storing it. Picking a file sets it
|
||||||
|
// from the extension — on for a `.binpb`, off for a `.room` — and the owner can overrule
|
||||||
|
// that either way: a `.binpb` that already loads shouldn't lose its circuits for nothing.
|
||||||
|
const [downgrade, setDowngrade] = useState(false)
|
||||||
|
const [fileError, setFileError] = useState('')
|
||||||
// The file input is uncontrolled — React can't set its value — so clearing the picked
|
// The file input is uncontrolled — React can't set its value — so clearing the picked
|
||||||
// file after a save takes a handle on the element itself.
|
// file after a save takes a handle on the element itself.
|
||||||
const input = useRef<HTMLInputElement>(null)
|
const input = useRef<HTMLInputElement>(null)
|
||||||
@@ -1889,7 +1904,7 @@ function BlobUpload({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!file) return
|
if (!file) return
|
||||||
void run(async () => {
|
void run(async () => {
|
||||||
const { file: blob, converted } = await prepareRoomBlob(file)
|
const blob = await prepareRoomBlob(file, downgrade)
|
||||||
const [filename, hash] = await Promise.all([uploadRoomBlob(blob), blobHash(blob)])
|
const [filename, hash] = await Promise.all([uploadRoomBlob(blob), blobHash(blob)])
|
||||||
onRoomChange(
|
onRoomChange(
|
||||||
await saveSubRoomBlob(roomId, subRoomId, {
|
await saveSubRoomBlob(roomId, subRoomId, {
|
||||||
@@ -1900,9 +1915,10 @@ function BlobUpload({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
setFile(null)
|
setFile(null)
|
||||||
|
setDowngrade(false)
|
||||||
setDescription('')
|
setDescription('')
|
||||||
if (input.current) input.current.value = ''
|
if (input.current) input.current.value = ''
|
||||||
const uploaded = converted ? 'Converted from .binpb, uploaded' : 'Uploaded'
|
const uploaded = downgrade ? 'Converted for this build, uploaded' : 'Uploaded'
|
||||||
return publish
|
return publish
|
||||||
? `${uploaded} and published — players load this scene now.`
|
? `${uploaded} and published — players load this scene now.`
|
||||||
: `${uploaded} and staged. Publish it in game to make it live.`
|
: `${uploaded} and staged. Publish it in game to make it live.`
|
||||||
@@ -1919,19 +1935,35 @@ function BlobUpload({
|
|||||||
<p className="muted blob-upload-caveat">
|
<p className="muted blob-upload-caveat">
|
||||||
New and lightly tested. Nothing here checks the file — the server stores whatever it is and
|
New and lightly tested. Nothing here checks the file — the server stores whatever it is and
|
||||||
the game finds out on load. This server runs the {CLIENT_BUILD_DATE} build, so scene data
|
the game finds out on load. This server runs the {CLIENT_BUILD_DATE} build, so scene data
|
||||||
from a room built on anything newer may not load at all. A <code>.binpb</code> file is
|
from a room built on anything newer may not load at all. Converting a file for this build
|
||||||
converted for this build before it is stored, which removes its circuits. Download the save
|
before it is stored gets around that, but removes its circuits. Download the save above and
|
||||||
above and keep it before replacing it.
|
keep it before replacing it.
|
||||||
</p>
|
</p>
|
||||||
<label className="blob-upload-file">
|
<label className="blob-upload-file">
|
||||||
Scene data file
|
Scene data file
|
||||||
<input
|
<input
|
||||||
ref={input}
|
ref={input}
|
||||||
type="file"
|
type="file"
|
||||||
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
|
accept={ROOM_BLOB_EXTENSIONS.join(',')}
|
||||||
|
onChange={(e) => {
|
||||||
|
// `accept` only filters the picker — a file dropped on the input, or picked
|
||||||
|
// under "All files", still arrives here, so the extension is checked again.
|
||||||
|
const picked = e.target.files?.[0] ?? null
|
||||||
|
if (picked && !isRoomBlobFile(picked.name)) {
|
||||||
|
setFileError('Pick a .room or .binpb file.')
|
||||||
|
setFile(null)
|
||||||
|
setDowngrade(false)
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setFileError('')
|
||||||
|
setFile(picked)
|
||||||
|
setDowngrade(picked !== null && needsDowngrade(picked.name))
|
||||||
|
}}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
{fileError && <p className="error">{fileError}</p>}
|
||||||
<label className="blob-upload-note">
|
<label className="blob-upload-note">
|
||||||
Save comment<span className="optional">optional</span>
|
Save comment<span className="optional">optional</span>
|
||||||
<input
|
<input
|
||||||
@@ -1946,6 +1978,15 @@ function BlobUpload({
|
|||||||
<input type="checkbox" checked={publish} onChange={(e) => setPublish(e.target.checked)} />
|
<input type="checkbox" checked={publish} onChange={(e) => setPublish(e.target.checked)} />
|
||||||
Publish it straight away
|
Publish it straight away
|
||||||
</label>
|
</label>
|
||||||
|
<label className="check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={downgrade}
|
||||||
|
disabled={file === null}
|
||||||
|
onChange={(e) => setDowngrade(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Downgrade room for compatibility
|
||||||
|
</label>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
{done && <p className="ok">{done}</p>}
|
{done && <p className="ok">{done}</p>}
|
||||||
<button type="submit" disabled={pending || file === null}>
|
<button type="submit" disabled={pending || file === null}>
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ export interface DowngradeStats {
|
|||||||
outputBytes: number
|
outputBytes: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether an uploaded scene file is the newer format that needs downgrading first. */
|
/**
|
||||||
|
* Whether a scene file is, by its name, the newer format. Only a default: the upload form
|
||||||
|
* ticks its convert box from this and the owner decides.
|
||||||
|
*/
|
||||||
export function needsDowngrade(filename: string): boolean {
|
export function needsDowngrade(filename: string): boolean {
|
||||||
return filename.toLowerCase().endsWith('.binpb')
|
return filename.toLowerCase().endsWith('.binpb')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user