transfer data to js wip

This commit is contained in:
Robin Appelman 2020-01-31 00:37:07 +01:00
commit 59c410566e
9 changed files with 338 additions and 59 deletions

View file

@ -39,10 +39,10 @@ npm test -- --safari
* `webpack.config.js` contains the Webpack configuration. You shouldn't need to change this, unless you have very special needs.
* The `js` folder contains your JavaScript code (`index.js` is used to hook everything into Webpack, you don't need to change it).
* The `js` folder contains your JavaScript code (`index.ts` is used to hook everything into Webpack, you don't need to change it).
* The `src` folder contains your Rust code.
* The `static` folder contains any files that you want copied as-is into the final build. It contains an `index.html` file which loads the `index.js` file.
* The `static` folder contains any files that you want copied as-is into the final build. It contains an `index.html` file which loads the `index.ts` file.
* The `tests` folder contains your Rust unit tests.

View file

@ -1,24 +0,0 @@
import("../pkg/index.js")
.then(m => {
document.getElementById('file').onchange = e => {
let file = e.target.files[0];
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function() {
console.log(reader.result);
let bytes = new Uint8Array(reader.result);
console.time('demo_parse');
m.parse_demo(bytes);
console.timeEnd('demo_parse');
};
reader.onerror = function() {
console.log(reader.error);
};
};
})
.catch(console.error);

138
js/index.ts Normal file
View file

@ -0,0 +1,138 @@
import {FlatState, XY} from '../pkg/index.d.ts';
import("../pkg/index.js")
.then(m => {
document.getElementById('file').onchange = e => {
let file = (e.target as HTMLInputElement).files[0];
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
let bytes = new Uint8Array(reader.result as ArrayBuffer);
console.time('demo_parse');
const state = m.parse_demo(bytes);
console.timeEnd('demo_parse');
console.time('transfer');
let playerCount = m.get_player_count(state);
let boundaries = m.get_boundaries(state);
let data = m.get_data(state);
let parsed = new ParsedDemo(playerCount, {
boundary_min: {
x: boundaries.boundary_min.x,
y: boundaries.boundary_min.y,
},
boundary_max: {
x: boundaries.boundary_max.x,
y: boundaries.boundary_max.y,
}
}, data);
console.timeEnd('transfer');
console.log(parsed, parsed.getPlayer(100, 2));
};
reader.onerror = function () {
console.log(reader.error);
};
};
})
.catch(console.error);
enum Team {
Other = 0,
Spectator = 1,
Red = 2,
Blue = 3,
}
enum Class {
Other = 0,
Scout = 1,
Sniper = 2,
Solder = 3,
Demoman = 4,
Medic = 5,
Heavy = 6,
Pyro = 7,
Spy = 8,
Engineer = 9,
}
interface WorldBoundaries {
boundary_min: {
x: number,
y: number
},
boundary_max: {
x: number,
y: number
}
}
interface PlayerState {
position: {
x: number,
y: number
},
angle: number,
health: number,
team: Team,
playerClass: Class,
}
function unpack_f32(val: number, min: number, max: number): number {
const ratio = val / (Math.pow(2, 16) - 1);
return ratio * (max - min) + min;
}
function unpack_angle(val: number): number {
const ratio = val / (Math.pow(2, 8) - 1);
return ratio * 360;
}
class ParsedDemo {
private playerCount: number;
private world: WorldBoundaries;
private data: Uint8Array;
private tickCount: number;
constructor(playerCount: number, world: WorldBoundaries, data: Uint8Array) {
this.playerCount = playerCount;
this.world = world;
this.data = data;
this.tickCount = data.length / playerCount / PACK_SIZE;
}
getPlayer(tick: number, playerIndex: number): PlayerState {
if (playerIndex >= this.playerCount) {
throw new Error("Player out of bounds");
}
const base = ((playerIndex * this.tickCount) + tick) * PACK_SIZE;
return unpackPlayer(this.data, base, this.world);
}
}
const PACK_SIZE = 8;
function unpackPlayer(bytes: Uint8Array, base: number, world: WorldBoundaries): PlayerState {
const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
let health = bytes[base + 4] + (bytes[base + 5] << 8);
const angle = unpack_angle(bytes[base + 6]);
const team = (bytes[base + 7] >> 4) as Team;
const playerClass = (bytes[base + 7] & 15) as Class;
return {
position: {x, y},
angle,
health,
team,
playerClass
}
}

78
package-lock.json generated
View file

@ -1,5 +1,5 @@
{
"name": "rust-webpack-template",
"name": "tf-demos-viewer",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
@ -3855,6 +3855,12 @@
"sha.js": "^2.4.8"
}
},
"picomatch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
"dev": true
},
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@ -5000,6 +5006,70 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true
},
"ts-loader": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.1.tgz",
"integrity": "sha512-Dd9FekWuABGgjE1g0TlQJ+4dFUfYGbYcs52/HQObE0ZmUNjQlmLAS7xXsSzy23AMaMwipsx5sNHvoEpT2CZq1g==",
"dev": true,
"requires": {
"chalk": "^2.3.0",
"enhanced-resolve": "^4.0.0",
"loader-utils": "^1.0.2",
"micromatch": "^4.0.0",
"semver": "^6.0.0"
},
"dependencies": {
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
},
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
@ -5028,6 +5098,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typescript": {
"version": "3.7.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz",
"integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==",
"dev": true
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",

View file

@ -10,9 +10,11 @@
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^0.4.2",
"copy-webpack-plugin": "^5.0.3",
"rimraf": "^2.6.3",
"ts-loader": "^6.2.1",
"typescript": "^3.7.5",
"webpack": "^4.33.0",
"webpack-cli": "^3.3.3",
"webpack-dev-server": "^3.7.1",
"rimraf": "^2.6.3"
"webpack-dev-server": "^3.7.1"
}
}

View file

@ -3,7 +3,8 @@
#![macro_use]
use crate::state::ParsedDemo;
use tf_demo_parser::demo::parser::gamestateanalyser::GameStateAnalyser;
use tf_demo_parser::demo::parser::gamestateanalyser::{GameStateAnalyser, World};
use tf_demo_parser::demo::vector::Vector;
use tf_demo_parser::{Demo, DemoParser, ParseError};
use wasm_bindgen::prelude::*;
@ -14,14 +15,77 @@ macro_rules! log {
}
#[wasm_bindgen]
pub fn parse_demo(buffer: Box<[u8]>) -> Result<ParsedDemo, JsValue> {
let buffer = buffer.into_vec();
let parsed = parse_demo_inner(buffer).map_err(|e| JsValue::from(e.to_string()))?;
Ok(parsed)
#[derive(Debug, Clone, Copy)]
pub struct XY {
pub x: f32,
pub y: f32,
}
pub fn parse_demo_inner(buffer: Vec<u8>) -> Result<ParsedDemo, ParseError> {
impl From<Vector> for XY {
fn from(vec: Vector) -> Self {
XY { x: vec.x, y: vec.y }
}
}
#[wasm_bindgen]
#[derive(Debug, Clone, Copy)]
pub struct WorldBoundaries {
pub boundary_min: XY,
pub boundary_max: XY,
}
impl From<World> for WorldBoundaries {
fn from(world: World) -> Self {
WorldBoundaries {
boundary_min: world.boundary_min.into(),
boundary_max: world.boundary_max.into(),
}
}
}
#[wasm_bindgen]
pub struct FlatState {
player_count: usize,
data: Box<[u8]>,
boundaries: WorldBoundaries,
}
impl FlatState {
pub fn new(parsed: ParsedDemo, world: World) -> Self {
FlatState {
player_count: parsed.players.len(),
boundaries: world.into(),
data: parsed.flat().into_boxed_slice(),
}
}
}
#[wasm_bindgen]
pub fn parse_demo(buffer: Box<[u8]>) -> Result<FlatState, JsValue> {
let buffer = buffer.into_vec();
let (parsed, world) = parse_demo_inner(buffer).map_err(|e| JsValue::from(e.to_string()))?;
let world = world.ok_or_else(|| JsValue::from_str("No world defined in demo"))?;
Ok(FlatState::new(parsed, world))
}
#[wasm_bindgen]
pub fn get_boundaries(state: &FlatState) -> WorldBoundaries {
state.boundaries.clone()
}
#[wasm_bindgen]
pub fn get_player_count(state: &FlatState) -> usize {
state.player_count
}
#[wasm_bindgen]
pub fn get_data(state: FlatState) -> Box<[u8]> {
state.data
}
pub fn parse_demo_inner(buffer: Vec<u8>) -> Result<(ParsedDemo, Option<World>), ParseError> {
let demo = Demo::new(buffer);
let parser = DemoParser::new_with_analyser(demo.get_stream(), GameStateAnalyser::default());
let (_header, mut ticker) = parser.ticker()?;
@ -37,7 +101,8 @@ pub fn parse_demo_inner(buffer: Vec<u8>) -> Result<ParsedDemo, ParseError> {
skip = !skip;
}
Ok(parsed_demo)
let world: Option<&World> = ticker.state().world.as_ref();
Ok((parsed_demo, world.map(|w| w.clone())))
}
// This is like the `main` function, except for JavaScript.

View file

@ -1,6 +1,5 @@
use tf_demo_parser::demo::parser::gamestateanalyser::{Class, GameState, Team, World};
use tf_demo_parser::demo::vector::VectorXY;
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Angle(u8);
@ -19,11 +18,10 @@ impl From<Angle> for f32 {
}
}
#[wasm_bindgen]
#[derive(Debug, Clone, Default)]
pub struct ParsedDemo {
tick: usize,
players: Vec<Vec<u8>>,
pub tick: usize,
pub players: Vec<Vec<u8>>,
}
impl ParsedDemo {
@ -61,6 +59,13 @@ impl ParsedDemo {
.iter()
.fold(0, |size, player| size + player.len())
}
pub fn flat(self) -> Vec<u8> {
self.players
.into_iter()
.flat_map(|player| player.into_iter())
.collect()
}
}
#[derive(Debug, Default, Clone, PartialEq)]

8
tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"noImplicitAny": true,
"module": "esnext",
"target": "es6",
"allowJs": true
}
}

View file

@ -7,7 +7,7 @@ const dist = path.resolve(__dirname, "dist");
module.exports = {
mode: "production",
entry: {
index: "./js/index.js"
index: "./js/index.ts"
},
output: {
path: dist,
@ -16,6 +16,15 @@ module.exports = {
devServer: {
contentBase: dist,
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules|pkg/,
},
],
},
plugins: [
new CopyPlugin([
path.resolve(__dirname, "static")