mirror of
https://codeberg.org/demostf/tf-demos-viewer.git
synced 2026-06-03 18:14:11 +02:00
transfer data to js wip
This commit is contained in:
parent
3e7b885236
commit
59c410566e
9 changed files with 338 additions and 59 deletions
|
|
@ -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.
|
||||
|
|
|
|||
24
js/index.js
24
js/index.js
|
|
@ -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
138
js/index.ts
Normal 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
78
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
src/lib.rs
81
src/lib.rs
|
|
@ -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.
|
||||
|
|
|
|||
13
src/state.rs
13
src/state.rs
|
|
@ -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
8
tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true,
|
||||
"module": "esnext",
|
||||
"target": "es6",
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue