Seperate module.exports into readers, helpers and options
Also add helpers to exports (uncompress and bgr2rgb functions) Still stuck on the whole oversaturation thing. Considering writing something to convert icon objects to a more usable model format.
This commit is contained in:
parent
d5ec733b07
commit
42286ba7f8
20
icon.js
20
icon.js
|
@ -1,7 +1,8 @@
|
|||
//todo: Make this a module/mjs file. C6 compatibility can stay, if needed.
|
||||
//LOOKING FOR: LZARI implementation (for MAX), description of CBS compression (node zlib doesn't tackle it, even with RC4'ing the data)
|
||||
ICONJS_DEBUG = false;
|
||||
ICONJS_STRICT = true;
|
||||
ICONJS_VERSION = "0.4.2";
|
||||
ICONJS_VERSION = "0.5.0";
|
||||
|
||||
function setDebug(value) {
|
||||
ICONJS_DEBUG = !!value;
|
||||
|
@ -77,6 +78,7 @@ function convertBGR5A1toRGB5A1(bgrData) {
|
|||
let r = ((bgrData[index] >> 10) & 0b11111);
|
||||
// rrrrrgggggbbbbba (a = 1 because 0 is 127 which is 1.0f opacity for the GS)
|
||||
let newValue = 0b0000000000000001;
|
||||
// mind you, the alpha bit ^ seems to be set randomly on textures, anyway. Maybe i'm reading these wrong?
|
||||
newValue |= (r << 1);
|
||||
newValue |= (g << 6);
|
||||
newValue |= (b << 11);
|
||||
|
@ -166,7 +168,8 @@ function readIconFile(input) {
|
|||
}};
|
||||
const magic = u32le(0);
|
||||
if (magic !== 0x010000) {
|
||||
// USER WARNING: APPARENTLY NOT ALL ICONS ARE 0x010000. THIS THROW WILL BE DROPPED LATER.
|
||||
// USER NOTICE: So far, I have yet to parse an icon that hasn't had 0x00010000 as it's magic.
|
||||
// Can someone provide me pointers to such an icon if one exists?
|
||||
throw `Not a PS2 icon file (was ${magic}, expected ${0x010000})`;
|
||||
}
|
||||
const numberOfShapes = u32le(4);
|
||||
|
@ -237,6 +240,7 @@ function readIconFile(input) {
|
|||
case 'U': {
|
||||
//where every 16-bit entry is a BGR5A1 color 0b[bbbbbgggggrrrrra]
|
||||
texture = new Uint16Array(input.slice(offset, (offset+0x8000)));
|
||||
//see convertBGR5A1toRGB5A1() for more info.
|
||||
break;
|
||||
}
|
||||
case 'C': {
|
||||
|
@ -257,6 +261,7 @@ function readIconFile(input) {
|
|||
**/
|
||||
//output of this will be another u16[0x4000] of the decompressed texture
|
||||
//after that just parse output as-if it was uncompressed.
|
||||
//see uncompressTexture() and convertBGR5A1toRGB5A1() for more info.
|
||||
size = u32le(offset);
|
||||
texture = {size, data: input.slice(offset+4, offset+(4+size))};
|
||||
}
|
||||
|
@ -333,7 +338,7 @@ function readEmsPsuFile(input){
|
|||
offset += ((fdesc.size & 0b11111111110000000000) + 1024);
|
||||
} else {
|
||||
offset += fdesc.size;
|
||||
// if we're already filling sectors fully, no to change anything about it
|
||||
// if we're already filling 1k blocks fully, why change the value?
|
||||
}
|
||||
output[fdesc.filename] = {
|
||||
size: fdesc.size,
|
||||
|
@ -370,7 +375,7 @@ function readPsvFile(input){
|
|||
const type1 = u32le(56);
|
||||
const type2 = u32le(60);
|
||||
if(type1 !== 0x2c && type2 !== 2) {
|
||||
throw `Not parsing, this is not a PS2 save export (was ${type1}:${type2}, expected 44:2)`;
|
||||
throw `Not parsing, this is not in the PS2 save export format (was ${type1}:${type2}, expected 44:2)`;
|
||||
}
|
||||
const displayedSize = u32le(64);
|
||||
const ps2dOffset = u32le(68);
|
||||
|
@ -509,5 +514,10 @@ function readSharkXPortSxpsFile(input) {
|
|||
|
||||
if(typeof module !== "undefined") {
|
||||
// for C6JS
|
||||
module.exports = {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, setDebug, setStrictness};
|
||||
module.exports = {
|
||||
readers: {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile},
|
||||
helpers: {uncompressTexture, convertBGR5A1toRGB5A1},
|
||||
options: {setDebug, setStrictness},
|
||||
version: ICONJS_VERSION
|
||||
};
|
||||
}
|
47
index.js
47
index.js
|
@ -1,4 +1,5 @@
|
|||
const iconjs = require("./icon.js");
|
||||
const icondumper2 = require("./icon.js");
|
||||
const iconjs = icondumper2.readers;
|
||||
const filesystem = require("fs");
|
||||
const processObj = require("process");
|
||||
|
||||
|
@ -7,11 +8,13 @@ require("util").inspect.defaultOptions.maxArrayLength = 10;
|
|||
require("util").inspect.defaultOptions.compact = true;
|
||||
require("util").inspect.defaultOptions.depth = 2;
|
||||
|
||||
// debugger
|
||||
iconjs.setDebug(false);
|
||||
// output debugging information
|
||||
icondumper2.options.setDebug(false);
|
||||
|
||||
// node.js client
|
||||
if(processObj.argv[2] === "psu") {
|
||||
console.log(`icon.js version ${icondumper2.version}, 2023 (c) yellows111`);
|
||||
switch(processObj.argv[2]) {
|
||||
case "psu": {
|
||||
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu");
|
||||
const parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
|
||||
const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data);
|
||||
|
@ -20,14 +23,19 @@ if(processObj.argv[2] === "psu") {
|
|||
output[file] = iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data);
|
||||
});
|
||||
console.log(output);
|
||||
} else if(processObj.argv[2] === "psv") {
|
||||
break;
|
||||
}
|
||||
case "psv": {
|
||||
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psv");
|
||||
const parsed = iconjs.readPsvFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
|
||||
console.log(parsed);
|
||||
const PS2D = iconjs.readPS2D(parsed["icon.sys"]);
|
||||
let output = {parsed, PS2D};
|
||||
console.log(output);
|
||||
} else if(processObj.argv[2] === "sps") {
|
||||
break;
|
||||
}
|
||||
case "sps":
|
||||
case "xps": {
|
||||
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.sps");
|
||||
const parsed = iconjs.readSharkXPortSxpsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
|
||||
console.log(parsed);
|
||||
|
@ -37,15 +45,34 @@ if(processObj.argv[2] === "psu") {
|
|||
output[file] = iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data);
|
||||
});
|
||||
console.log(output);
|
||||
} else {
|
||||
let inputFile = filesystem.readFileSync(processObj.argv[2] ? processObj.argv[2] : "icon.sys");
|
||||
break;
|
||||
}
|
||||
case "sys": {
|
||||
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.sys");
|
||||
const metadata = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
|
||||
console.log("\noutput:", metadata, "\n")
|
||||
|
||||
if(processObj.argv.length > 4 && processObj.argv[4].toLowerCase() === "--no-read-models") {break;} else {
|
||||
Object.keys(metadata.filenames).forEach(function(file) {
|
||||
let getFile = filesystem.readFileSync(metadata.filenames[file]);
|
||||
const output = iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength));
|
||||
//console.log(individialIcon);
|
||||
console.log(`contents of ${metadata.filenames[file]} (${file}):`, output, "\n");
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
//Template literal goes here.
|
||||
console.log(
|
||||
`${(processObj.argv.length > 2) ? "Unknown argument: "+processObj.argv[2]+"\n\n": ""}icondumper2 node.js client subcommands:
|
||||
psu: Read a EMS Memory Adapter export file.
|
||||
psv: Read a PS3 export file.
|
||||
sps: Read a SharkPort export file.
|
||||
xps: Read a X-Port export file.
|
||||
|
||||
sys: Read a icon.sys (964 bytes) file, and attempt
|
||||
to read icon files from the current directory.
|
||||
(suppress behaviour with --no-read-models)
|
||||
` ); // end of template
|
||||
}
|
||||
}
|
102
input.htm
102
input.htm
|
@ -28,6 +28,7 @@
|
|||
attribute vec4 a_color;
|
||||
|
||||
uniform float u_rotation;
|
||||
uniform float u_scale;
|
||||
uniform highp vec3 u_ambientLight;
|
||||
//uniform highp vec3 u_lightColorA;
|
||||
//uniform highp vec3 u_lightColorB;
|
||||
|
@ -44,7 +45,7 @@
|
|||
(a_position.x * pos.x) + (a_position.z * pos.y), //transform the x position
|
||||
(0.0 - a_position.y) - 2.75, // invert the y position and move down -2.75, which will center the model
|
||||
(a_position.x * -pos.y) + (a_position.z * pos.x), //transform the z position
|
||||
3.5
|
||||
u_scale
|
||||
);
|
||||
// flip it, scale it
|
||||
v_textureCoords = a_textureCoords;
|
||||
|
@ -72,10 +73,13 @@
|
|||
mediump vec4 texture_c = texture2D(u_sampler, v_textureCoords);
|
||||
highp vec3 ambientColorT = (u_ambientLight * vec3(texture_c));
|
||||
highp vec3 ambientColorV = (u_ambientLight * vec3(v_color));
|
||||
//This has issues with oversaturation, but it means blended icons work.
|
||||
//This has issues with oversaturation (JXCR), but it means blended icons work.
|
||||
//This also makes scaling a bit strange (JXCR, WLK). Why does it change depending on the scale?
|
||||
if(v_color == vec4(1.0,1.0,1.0,1.0)) {
|
||||
gl_FragColor = vec4((ambientColorT * ambientColorV), texture_c.a);
|
||||
} else {
|
||||
//WLK *SHOULD* follow this path, but doesn't. What can I do to fix this?
|
||||
//Removing this path makes the models very dark, doing this fixes it, with oversaturation on false positives.
|
||||
gl_FragColor = vec4((ambientColorT * ambientColorV) * 2.0, texture_c.a);
|
||||
}
|
||||
}
|
||||
|
@ -113,9 +117,9 @@
|
|||
<h1 id="title1">No File</h1>
|
||||
<h1 id="title2">Loaded</h1>
|
||||
</div>
|
||||
<span>Background/icon preview (rotate: ←/→ keys):</span><br>
|
||||
<canvas id="bgcanvas" width="360" height="360"></canvas>
|
||||
<canvas id="iconcanvas" width="360" height="360"></canvas>
|
||||
<span>Background/icon preview (rotate: ←/→ keys, scale: ↑/↓ keys):</span><br>
|
||||
<canvas id="bgcanvas" width="480" height="480"></canvas>
|
||||
<canvas id="iconcanvas" width="480" height="480"></canvas>
|
||||
<hr>
|
||||
<p>Normal: <kbd id="iconn">(no file)<wbr></kbd> Copying: <kbd id="iconc">(no file)<wbr></kbd> Deleting: <kbd id="icond">(no file)</kbd></p>
|
||||
<div id="advanced">
|
||||
|
@ -146,28 +150,52 @@
|
|||
<span>File comments: </span><span id="fileCommentGame">(no title)</span></span><span> - </span><span id="fileCommentName">(no description)</span>
|
||||
</p>
|
||||
<script>
|
||||
// I usually don't do in-body <script>'s, but I didn't want to do an await onload() again
|
||||
const GlobalState = {rotations: 2, dataLength: 0, uniforms: {rotation: null, scale: null}};
|
||||
// I don't care HOW disgusting doing this is, I'm sick of pressing escape to clear these.
|
||||
let allInputs = document.querySelectorAll('input[type="file"]');
|
||||
allInputs.forEach(
|
||||
function(nodeObject) {
|
||||
nodeObject.onclick = function() {
|
||||
allInputs.forEach(function(elementObject) {elementObject.value = null;});
|
||||
}
|
||||
}
|
||||
);
|
||||
// rotation stuff
|
||||
const rotationDensity = 60;
|
||||
rotations = 2;
|
||||
document.body.onkeydown = function(ev) {
|
||||
if(glBgContext === null) {return;}
|
||||
if(typeof URotation !== "undefined") {
|
||||
if(typeof GlobalState.uniforms.rotation !== "undefined") {
|
||||
switch(ev.code) {
|
||||
case "ArrowLeft": {
|
||||
rotations--;
|
||||
if(rotations < -rotationDensity) {rotations = -1;}
|
||||
GlobalState.rotations--;
|
||||
if(GlobalState.rotations < -rotationDensity) {GlobalState.rotations = -1;}
|
||||
break;
|
||||
}
|
||||
case "ArrowRight": {
|
||||
rotations++;
|
||||
if(rotations > rotationDensity) {rotations = 1;}
|
||||
GlobalState.rotations++;
|
||||
if(GlobalState.rotations > rotationDensity) {GlobalState.rotations = 1;}
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
GlobalState.scale -= 0.1;
|
||||
if(GlobalState.scale <= 2) {GlobalState.scale = 2.0;}
|
||||
break;
|
||||
}
|
||||
case "ArrowDown": {
|
||||
GlobalState.scale += 0.1;
|
||||
if(GlobalState.scale >= 6.0) {GlobalState.scale = 6.0;}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
};
|
||||
glFgContext.uniform1f(URotation, (rotations/rotationDensity));
|
||||
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, ULength);
|
||||
glFgContext.uniform1f(GlobalState.uniforms.scale, GlobalState.scale);
|
||||
glFgContext.uniform1f(GlobalState.uniforms.rotation, (GlobalState.rotations/rotationDensity));
|
||||
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength);
|
||||
} else {return;}
|
||||
}
|
||||
// I usually don't do in-body <script>'s, but I didn't want to do an await onload again
|
||||
function updateDisplay(input) {
|
||||
document.getElementById("title1").textContent = input.title[0];
|
||||
document.getElementById("title2").textContent = input.title[1];
|
||||
|
@ -254,7 +282,8 @@
|
|||
};
|
||||
var uniforms = {
|
||||
rotation: glFgContext.getUniformLocation(iconProgram, "u_rotation"),
|
||||
ambientLighting: glFgContext.getUniformLocation(iconProgram, "u_ambientLight")
|
||||
ambientLighting: glFgContext.getUniformLocation(iconProgram, "u_ambientLight"),
|
||||
scale: glFgContext.getUniformLocation(iconProgram, "u_scale")
|
||||
}
|
||||
} else {
|
||||
var attributes = {
|
||||
|
@ -264,7 +293,8 @@
|
|||
var uniforms = {
|
||||
sampler: glFgContext.getUniformLocation(iconProgram, "u_sampler"),
|
||||
rotation: glFgContext.getUniformLocation(iconProgram, "u_rotation"),
|
||||
ambientLighting: glFgContext.getUniformLocation(iconProgram, "u_ambientLight")
|
||||
ambientLighting: glFgContext.getUniformLocation(iconProgram, "u_ambientLight"),
|
||||
scale: glFgContext.getUniformLocation(iconProgram, "u_scale")
|
||||
}
|
||||
}
|
||||
//.section SETUP
|
||||
|
@ -309,19 +339,24 @@
|
|||
//.section ROTATE
|
||||
// sets the angle uniform to 2/rotationDensity, this puts the icon at an angle.
|
||||
// globalize uniform rotation
|
||||
URotation = uniforms.rotation;
|
||||
rotations = 2;
|
||||
glFgContext.uniform1f(URotation, rotations/rotationDensity);
|
||||
GlobalState.uniforms.rotation = uniforms.rotation;
|
||||
GlobalState.rotations = 2;
|
||||
glFgContext.uniform1f(GlobalState.uniforms.rotation, GlobalState.rotations/rotationDensity);
|
||||
|
||||
//.section LIGHTING
|
||||
let colours = fileMetadata.lighting.colors[0];
|
||||
//glFgContext.uniform3f(uniforms.ambientLighting, colours.r, colours.g, colours.b);
|
||||
glFgContext.uniform3f(uniforms.ambientLighting, 1, 1, 1);
|
||||
|
||||
//.section SCALING
|
||||
GlobalState.uniforms.scale = uniforms.scale;
|
||||
GlobalState.scale = 3.5;
|
||||
glFgContext.uniform1f(GlobalState.uniforms.scale, GlobalState.scale);
|
||||
|
||||
//.section WRITE
|
||||
//globalize count of triangles, as well
|
||||
ULength = (verticesArray.length/3);
|
||||
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, ULength);
|
||||
GlobalState.dataLength = (verticesArray.length/3);
|
||||
glFgContext.drawArrays(glFgContext.TRIANGLES, 0, GlobalState.dataLength);
|
||||
}
|
||||
}
|
||||
document.getElementById("strictnessOption").onchange = function(e) {
|
||||
|
@ -358,7 +393,7 @@
|
|||
try {
|
||||
let output = readIconFile(d);
|
||||
renderIcon(output);
|
||||
console.log("model data",output);
|
||||
console.log("model data (ic*)",output);
|
||||
} catch(e) {
|
||||
if(glFgContext!==null){glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);}
|
||||
alert(e);
|
||||
|
@ -386,7 +421,8 @@
|
|||
//TODO: use Time() to align JST times to user-local timezone
|
||||
document.getElementById("dateCreated").textContent = `${cTime.hours.toString().padStart("2","0")}:${cTime.minutes.toString().padStart("2","0")}:${cTime.seconds.toString().padStart("2","0")} ${cTime.day.toString().padStart("2","0")}/${cTime.month.toString().padStart("2","0")}/${cTime.year}`;
|
||||
document.getElementById("dateModified").textContent = `${mTime.hours.toString().padStart("2","0")}:${mTime.minutes.toString().padStart("2","0")}:${mTime.seconds.toString().padStart("2","0")} ${mTime.day.toString().padStart("2","0")}/${mTime.month.toString().padStart("2","0")}/${mTime.year}`;
|
||||
console.log("model files", output2);
|
||||
console.log("model files (psu)", output2);
|
||||
console.log("icon.sys (psu)", output);
|
||||
} catch(e) {
|
||||
if(glBgContext!==null){glBgContext.clear(glBgContext.COLOR_BUFFER_BIT);}
|
||||
if(glFgContext!==null){glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);}
|
||||
|
@ -405,13 +441,19 @@
|
|||
let inputData = readPsvFile(d);
|
||||
let output = readPS2D(inputData["icon.sys"]);
|
||||
updateDisplay(output);
|
||||
renderIcon(readIconFile(inputData.icons.n), output);
|
||||
console.log(readIconFile(inputData.icons.n));
|
||||
const icons = {
|
||||
n: readIconFile(inputData.icons.n),
|
||||
c: readIconFile(inputData.icons.c),
|
||||
d: readIconFile(inputData.icons.d),
|
||||
}
|
||||
renderIcon(icons.n, output);
|
||||
let cTime = inputData.timestamps.created;
|
||||
let mTime = inputData.timestamps.modified;
|
||||
//TODO: use Time() to align JST times to user-local timezone
|
||||
document.getElementById("dateCreated").textContent = `${cTime.hours.toString().padStart("2","0")}:${cTime.minutes.toString().padStart("2","0")}:${cTime.seconds.toString().padStart("2","0")} ${cTime.day.toString().padStart("2","0")}/${cTime.month.toString().padStart("2","0")}/${cTime.year}`;
|
||||
document.getElementById("dateModified").textContent = `${mTime.hours.toString().padStart("2","0")}:${mTime.minutes.toString().padStart("2","0")}:${mTime.seconds.toString().padStart("2","0")} ${mTime.day.toString().padStart("2","0")}/${mTime.month.toString().padStart("2","0")}/${mTime.year}`;
|
||||
console.log("model files (psv)", icons);
|
||||
console.log("icon.sys (psv)", output);
|
||||
} catch(e) {
|
||||
if(glBgContext!==null){glBgContext.clear(glBgContext.COLOR_BUFFER_BIT);}
|
||||
if(glFgContext!==null){glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);}
|
||||
|
@ -442,7 +484,8 @@
|
|||
document.getElementById("dateModified").textContent = `${mTime.hours.toString().padStart("2","0")}:${mTime.minutes.toString().padStart("2","0")}:${mTime.seconds.toString().padStart("2","0")} ${mTime.day.toString().padStart("2","0")}/${mTime.month.toString().padStart("2","0")}/${mTime.year}`;
|
||||
document.getElementById("fileCommentGame").textContent = vFilesystem.comments.game;
|
||||
document.getElementById("fileCommentName").textContent = vFilesystem.comments.name;
|
||||
console.log("model files", output2);
|
||||
console.log("model files (*ps)", output2);
|
||||
console.log("icon.sys (*ps)", output);
|
||||
} catch(e) {
|
||||
if(glBgContext!==null){glBgContext.clear(glBgContext.COLOR_BUFFER_BIT);}
|
||||
if(glFgContext!==null){glFgContext.clear(glFgContext.COLOR_BUFFER_BIT | glFgContext.DEPTH_BUFFER_BIT);}
|
||||
|
@ -459,7 +502,7 @@
|
|||
return shader;
|
||||
}
|
||||
|
||||
console.log(gl.getShaderInfoLog(shader), source);
|
||||
console.log(gl.getShaderInfoLog(shader), (new String().padStart(60, "-")), source);
|
||||
gl.deleteShader(shader);
|
||||
}
|
||||
function createProgram(gl, vertexShader, fragmentShader) {
|
||||
|
@ -509,7 +552,6 @@
|
|||
}
|
||||
if(glBgContext !== null) {
|
||||
//.section CONFIGURATION
|
||||
glBgContext.enable(glBgContext.DEPTH_TEST);
|
||||
glFgContext.enable(glFgContext.DEPTH_TEST);
|
||||
//.section CLEAR
|
||||
glBgContext.clearColor(0.1,0.1,0.4,1);
|
||||
|
@ -523,10 +565,10 @@
|
|||
document.getElementById("showExtractedInputOption").onchange = function(e) {
|
||||
document.getElementById("advanced").style.display = ((e.target.checked) ? "block" : "none");
|
||||
}
|
||||
//todo: Animation parsing, animation tweening
|
||||
//todo: More than one model shape rendering, other 2 icons, Animation parsing, animation tweening
|
||||
</script>
|
||||
<span id="version">icondumper2 <span id="iconjsVersion">(unknown icon.js version)</span> [C: <span id="clientVersion">Loading...</span>] — © 2023 yellows111</span>
|
||||
<script>document.getElementById("iconjsVersion").textContent = ICONJS_VERSION;</script>
|
||||
<script>document.getElementById("clientVersion").textContent = "0.6.2";</script>
|
||||
<script>document.getElementById("clientVersion").textContent = "0.6.3";</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue