Get level encoding and URL sharing just barely working!

This commit is contained in:
Eevee (Evelyn Woods) 2020-09-16 00:11:52 -06:00
parent fed52c42ab
commit a7f00d6ec4
3 changed files with 187 additions and 10 deletions

View File

@ -106,6 +106,7 @@
</header> </header>
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="level"><!-- level canvas and any overlays go here --></div>
<div class="controls"> <div class="controls">
<button id="editor-share-url" type="button">Share?</button>
<!-- <!--
<p style> <p style>
Tip: Right click to color drop.<br> Tip: Right click to color drop.<br>

View File

@ -71,6 +71,9 @@ let modifier_wire = {
tile.wire_directions = modifier & 0x0f; tile.wire_directions = modifier & 0x0f;
tile.wire_tunnel_directions = (modifier & 0xf0) >> 4; tile.wire_tunnel_directions = (modifier & 0xf0) >> 4;
}, },
encode(tile) {
return tile.wire_directions | (tile.wire_tunnel_directions << 4);
},
}; };
let arg_direction = { let arg_direction = {
@ -79,6 +82,9 @@ let arg_direction = {
let direction = ['north', 'east', 'south', 'west'][dirbyte & 0x03]; let direction = ['north', 'east', 'south', 'west'][dirbyte & 0x03];
tile.direction = direction; tile.direction = direction;
}, },
encode(tile) {
return {north: 0, east: 1, south: 2, west: 3}[tile.direction];
},
}; };
// TODO assert that direction + next match the tile types // TODO assert that direction + next match the tile types
@ -366,6 +372,10 @@ const TILE_ENCODING = {
decode(tile, mask) { decode(tile, mask) {
// TODO railroad props // TODO railroad props
}, },
encode(tile) {
// TODO
return 0;
},
}, },
}, },
0x50: { 0x50: {
@ -456,6 +466,9 @@ const TILE_ENCODING = {
decode(tile, ascii_code) { decode(tile, ascii_code) {
tile.ascii_code = ascii_code; tile.ascii_code = ascii_code;
}, },
encode(tile) {
return tile.ascii_code;
},
}, },
}, },
0x72: { 0x72: {
@ -514,6 +527,10 @@ const TILE_ENCODING = {
} }
tile.arrows = arrows; tile.arrows = arrows;
}, },
encode(tile) {
// TODO
return 0;
},
}, },
], ],
has_next: true, has_next: true,
@ -762,9 +779,14 @@ export function parse_level(buf) {
level.size_y = height; level.size_y = height;
let p = 2; let p = 2;
let n;
function read_spec() { function read_spec() {
let tile_byte = bytes[p]; let tile_byte = bytes[p];
p++; p++;
if (tile_byte === undefined)
throw new Error(`Read past end of file in cell ${n}`);
let spec = TILE_ENCODING[tile_byte]; let spec = TILE_ENCODING[tile_byte];
if (! spec) if (! spec)
throw new Error(`Unrecognized tile type 0x${tile_byte.toString(16)}`); throw new Error(`Unrecognized tile type 0x${tile_byte.toString(16)}`);
@ -772,7 +794,7 @@ export function parse_level(buf) {
return spec; return spec;
} }
for (let n = 0; n < width * height; n++) { for (n = 0; n < width * height; n++) {
let cell = new util.StoredCell; let cell = new util.StoredCell;
while (true) { while (true) {
let spec = read_spec(); let spec = read_spec();
@ -901,22 +923,137 @@ export function parse_level(buf) {
return level; return level;
} }
// Write 1, 2, or 4 bytes to a DataView
function write_n_bytes(view, start, n, value) {
if (n === 1) {
view.setUint8(start, value, true);
}
else if (n === 2) {
view.setUint16(start, value, true);
}
else if (n === 4) {
view.setUint32(start, value, true);
}
else {
throw new Error(`Can't write ${n} bytes`);
}
}
class C2M {
constructor() {
this._sections = []; // array of [name, arraybuffer]
}
add_section(name, buf) {
if (name.length !== 4) {
throw new Error(`Section names must be four characters, not '${name}'`);
}
if (typeof buf === 'string' || buf instanceof String) {
let str = buf;
// C2M also includes the trailing NUL
buf = new ArrayBuffer(str.length + 1);
let array = new Uint8Array(buf);
for (let i = 0, l = str.length; i < l; i++) {
array[i] = str.charCodeAt(i);
}
}
this._sections.push([name, buf]);
}
serialize() {
let parts = [];
let total_length = 0;
for (let [name, buf] of this._sections) {
total_length += buf.byteLength + 8;
}
let ret = new ArrayBuffer(total_length);
let array = new Uint8Array(ret);
let view = new DataView(ret);
let p = 0;
for (let [name, buf] of this._sections) {
// Write the header
for (let i = 0; i < 4; i++) {
view.setUint8(p + i, name.charCodeAt(i));
}
view.setUint32(p + 4, buf.byteLength, true);
p += 8;
// Copy in the section contents
array.set(new Uint8Array(buf), p);
p += buf.byteLength;
}
console.log(this);
console.log(total_length);
console.log(array);
return ret;
}
}
export function synthesize_level(stored_level) { export function synthesize_level(stored_level) {
add_section('CC2M', '133'); let c2m = new C2M;
c2m.add_section('CC2M', '133');
// FIXME well this will not do // FIXME well this will not do
let map_bytes = new Uint8Array(1024); let map_bytes = new Uint8Array(4096);
let map_view = new DataView(map_bytes.buffer);
map_bytes[0] = stored_level.size_x; map_bytes[0] = stored_level.size_x;
map_bytes[1] = stored_level.size_y; map_bytes[1] = stored_level.size_y;
let p = 2; let p = 2;
console.log(stored_level);
for (let cell of stored_level.linear_cells) { for (let cell of stored_level.linear_cells) {
// TODO can i allocate less here? iterate in reverse, avoid slicing? for (let i = cell.length - 1; i >= 0; i--) {
// TODO assert that the bottom tile has no next, and all the others do let tile = cell[i];
for (let tile of cell.reverse()) { // FIXME does not yet support canopy or thin walls >:S
let [tile_byte, ...args] = REVERSE_TILE_ENCODING[tile.type.name]; let spec = REVERSE_TILE_ENCODING[tile.type.name];
if (spec.modifier) {
let mod = spec.modifier.encode(tile);
if (mod === 0) {
// Zero is optional; do nothing
}
else if (mod < 256) {
// Encode in one byte
map_bytes[p] = REVERSE_TILE_ENCODING['#mod8'].tile_byte;
map_bytes[p + 1] = mod;
p += 2;
}
else if (mod < 65536) {
// Encode in two bytes
map_bytes[p] = REVERSE_TILE_ENCODING['#mod16'].tile_byte;
map_view.setUint16(p + 1, mod, true);
p += 3;
}
else {
// Encode in four (!) bytes
map_bytes[p] = REVERSE_TILE_ENCODING['#mod32'].tile_byte;
map_view.setUint16(p + 1, mod, true);
p += 5;
} }
} }
add_section('MAP ', ''); map_bytes[p] = spec.tile_byte;
add_section('END ', ''); p++;
if (spec.extra_args) {
for (let argspec of spec.extra_args) {
let arg = argspec.encode(tile);
write_n_bytes(map_view, p, argspec.size, arg);
p += argspec.size;
}
}
// TODO assert that the bottom tile has no next, and all the others do
}
}
// FIXME ack, ArrayBuffer.slice makes a copy actually! and i use it a lot in this file i think!!
c2m.add_section('MAP ', map_bytes.buffer.slice(0, p));
c2m.add_section('END ', '');
return c2m.serialize();
} }

View File

@ -677,6 +677,27 @@ class Player extends PrimaryView {
} }
class EditorShareOverlay extends DialogOverlay {
constructor(conductor, url) {
super(conductor);
this.set_title("give this to friends");
this.main.append(mk('p', "Give this URL out to let others try your level:"));
this.main.append(mk('p.editor-share-url', {}, url));
let copy_button = mk('button', {type: 'button'}, "Copy to clipboard");
copy_button.addEventListener('click', ev => {
navigator.clipboard.writeText(url);
// TODO feedback?
});
this.main.append(copy_button);
let ok = mk('button', {type: 'button'}, "neato");
ok.addEventListener('click', ev => {
this.close();
});
this.footer.append(ok);
}
}
const EDITOR_TOOLS = [{ const EDITOR_TOOLS = [{
mode: 'pencil', mode: 'pencil',
icon: 'icons/tool-pencil.png', icon: 'icons/tool-pencil.png',
@ -955,6 +976,18 @@ class Editor extends PrimaryView {
this.mouse_mode = null; this.mouse_mode = null;
}); });
// Toolbar buttons
this.root.querySelector('#editor-share-url').addEventListener('click', ev => {
let buf = c2m.synthesize_level(this.stored_level);
// FIXME Not ideal, but btoa() wants a string rather than any of the myriad binary types
let data = btoa(Array.from(new Uint8Array(buf)).map(n => String.fromCharCode(n)).join(''));
let url = new URL(location);
url.searchParams.delete('level');
url.searchParams.delete('setpath');
url.searchParams.append('level', data);
new EditorShareOverlay(this.conductor, url.toString()).open();
});
// Toolbox // Toolbox
let toolbox = mk('div.icon-button-set') let toolbox = mk('div.icon-button-set')
this.root.querySelector('.controls').append(toolbox); this.root.querySelector('.controls').append(toolbox);
@ -1164,7 +1197,6 @@ class Splash extends PrimaryView {
// TODO handle errors // TODO handle errors
// TODO cancel a download if we start another one? // TODO cancel a download if we start another one?
let buf = await fetch(path); let buf = await fetch(path);
let stored_game;
this.load_file(buf); this.load_file(buf);
// TODO get title out of C2G when it's supported // TODO get title out of C2G when it's supported
this.conductor.level_pack_name_el.textContent = title || path; this.conductor.level_pack_name_el.textContent = title || path;
@ -1538,9 +1570,16 @@ async function main() {
// Pick a level (set) // Pick a level (set)
// TODO error handling :( // TODO error handling :(
let path = query.get('setpath'); let path = query.get('setpath');
let b64level = query.get('level');
if (path && path.match(/^levels[/]/)) { if (path && path.match(/^levels[/]/)) {
conductor.splash.fetch_pack(path); conductor.splash.fetch_pack(path);
} }
else if (b64level) {
// TODO all the more important to show errors!!
// FIXME Not ideal, but atob() returns a string rather than any of the myriad binary types
let buf = Uint8Array.from(atob(b64level), c => c.charCodeAt(0)).buffer;
conductor.splash.load_file(buf);
}
} }
main(); main();