@awayfl/poki-player
Version:
AVM Player for poki games
385 lines (319 loc) • 8.06 kB
text/typescript
import { AVMPlayer } from "@awayfl/awayfl-player";
import { AVMPlayerPoki } from "./AVMPlayerPoki";
import JSZip from "jszip";
const STYLE_TMPLATE = `
#report__root {
width: 100%;
height: 100%;
position: absolute;
z-index: 1000;
user-select: none;
pointer-events: none;
}
.report__btn {
padding: 4px;
height: 1.5em;
background: #222;
color: #eee;
text-align: center;
font-size: 1.5em;
line-height: 1.5em;
border-radius: 1.5em;
pointer-events: all;
box-shadow: 1px 3px 5px 2px black;
transition: background-color 0.5s;
}
.report__btn:hover {
cursor: pointer;
background: #444;
}
#report__snap:hover {
right: -15px;
color: #ffc;
}
#report__snap {
position: absolute;
top: 0.5em;
right: -30px;
width: 100px;
padding: 4px 16px 4px 4px;
transition: right 0.5s;
display: none;
}
#report__poppup {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: max(60%, 300px);
min-height: 40%;
max-height: 80%;
height: auto;
display: none;
flex-direction: column;
background: #fcc;
box-shadow: 1px 3px 5px 2px black;
border-radius: 4px;
justify-content: space-around;
align-items: center;
color: #222;
padding: 10px;
}
#report__poppup.show{
display: flex;
}
#report__text {
width: 100%;
min-height: 30%;
height: auto;
background: rgba(255,255,255, 0.5);
pointer-events: all;
}
`
const HTML_TEMPLATE = `
<div id='report__snap' class = 'report__btn'>
<span>SNAP</span>
</div>
<div id='report__poppup'>
<h1>Report!</h1>
<textarea readonly = "true" id="report__text" rows='10'>error</textarea>
<div id = 'report__save' class = 'report__btn'>
SAVE REPORT
</div>
</div>
`
const GL_CONST = {
MAX_TEXTURE_SIZE: 0x0d33,
MAX_VIEWPORT_DIMS: 0x0d3a,
MAX_TEXTURE_IMAGE_UNITS: 0x8872,
MAX_RENDERBUFFER_SIZE: 0x84e8,
MAX_ELEMENTS_INDICES: 0x80e9,
MAX_DRAW_BUFFERS: 0x8824,
MAX_COLOR_ATTACHMENTS: 0x8cdf,
}
const defaultError = window.onerror;
const original = {
log: console.log,
warn: console.warn,
debug: console.debug,
error: console.error,
info: console.info,
};
export class AVMCrashReport {
public static collectLogs = true;
public static instance: AVMCrashReport = null;
public logs = [];
public glInfo = {};
public lastCrash = null;
public roodtEl = null;
protected player: AVMPlayerPoki;
public constructor() {
this._attachReporters();
this._attachUI();
this._webGlInfo();
//@ts-ignore
window.REPORTER = this;
}
public static init() {
AVMCrashReport.instance = new AVMCrashReport();
}
public static bind(player: AVMPlayer) {
if(AVMCrashReport.instance) {
AVMCrashReport.instance.bind(player);
}
}
public bind(player: any) {
this.player = player;
}
private _attachUI() {
const root = this.roodtEl = document.createElement("div");
root.setAttribute('id', "report__root");
root.innerHTML = HTML_TEMPLATE;
document.body.appendChild(root);
const s = document.createElement('style');
s.innerHTML = STYLE_TMPLATE;
document.head.appendChild(s);
const b = root.querySelector("#report__snap");
b.addEventListener("click", () => this._requsetSnap());
}
private _requsetSnap() {
console.log("[AVMCrashReporter] Generate report...");
const pop = this.roodtEl.querySelector("#report__poppup");
const area = pop.querySelector("#report__text");
const save = pop.querySelector("#report__save");
if(!pop) {
return null;
}
area.textContent = this.logs.join("\n");
pop.classList.toggle("show", true);
save.addEventListener("click",() => {
pop.classList.toggle("show", false);
const data = this.generateReport()
const name =
`report_${ this.player.config.title}_${(new Date().toISOString())}`
this._saveFile(data, name);
}, {once: true})
}
private _saveFile(data: any, name: string) {
if (!JSZip) {
// remove to save size
data.snap = null;
this._trigLoad(
new Blob( [ JSON.stringify (data, null, 2) ], {type: 'application/json'} ),
name + '.jzon'
);
} else {
const snap: HTMLCanvasElement = data.snap;
if (snap) {
data.snap = name + '.png';
}
const zip = new JSZip();
zip.file(`${name}.json`, JSON.stringify(data, null, 2));
if (snap) {
zip.file(data.snap, snap.toDataURL().replace('data:image/png;base64,', ''), {base64: true});
}
zip.generateAsync({ type:"blob" })
.then((content: Blob) => {
// see FileSaver.js
this._trigLoad(content, name + '.zip');
});
}
}
private _trigLoad(blob, name) {
const url = URL.createObjectURL( blob );
const link = document.createElement( 'a' );
link.setAttribute( 'href', url );
link.setAttribute( 'download', name );
const event = document.createEvent( 'MouseEvents' );
event.initMouseEvent( 'click',
true, true, window,
1, 0, 0, 0, 0,
false, false, false,
false, 0, null);
link.dispatchEvent( event );
}
private _attachReporters() {
window.addEventListener('error', this._catchUnhandled.bind(this));
const _this = this;
if (AVMCrashReport.collectLogs) {
for(let key in original) {
console[key] = function (...args:any[]) {
_this._trackLogs(key, ...args);
original[key].apply(console, args);
}
}
}
}
private _trackLogs(type:string, ...args: any[]) {
args = args.filter(e => !!Object.getPrototypeOf(e));
if (args.length === 0 && args[0].mode ** args[0].stack) {
args[0] = JSON.stringify(args[0]);
}
this.logs.push("[" + type.toUpperCase() + "]: " + args.join(" "));
}
private _catchUnhandled(error: ErrorEvent) {
if (error.filename.indexOf("http://jit/") < 0)
return;
this._trackLogs("exception", error.message, error.filename, error.lineno, error.error.stack);
this.lastCrash = {
error: error.error,
line: error.lineno,
message: error.message,
filename: error.filename,
source: null
}
this._requsetSnap();
}
public generateReport() {
const data = {
date: new Date(),
game: this._gameInfo(),
device: this._deviceInfo(),
context: this.glInfo,
config: this.player.config,
logs: this.logs,
crash: this.lastCrash,
snap: null // comming soon
};
return data;
}
private _gameInfo() {
const player = <any>this.player;
const avm = player._avmHandler.avmVersion;
const {
swfVersion,
fpVersion,
frameCount,
frameRate,
compression,
bytesTotal
} = player._swfFile;
let path: string = (<any>player._gameConfig).binary.filter(({resourceType}) => resourceType === 'GAME')[0]?.path;
if(path && path.indexOf('?') > -1) {
path = path.substring(0, path.indexOf('?'));
}
return {
file: {
name: (<any>player._gameConfig)?.title,
path: path,
size: bytesTotal
},
runtime: {
swfVersion,
fpVersion,
frameCount,
frameRate,
compression,
avm
}
}
}
private _webGlInfo() {
const c = document.createElement('canvas');
let version = 2;
let ctx: WebGL2RenderingContext | WebGLRenderingContext = c.getContext('webgl2');
if(!ctx) {
ctx = c.getContext('webgl');
version = 1;
}
let params = Object.create(null);
for(const key in GL_CONST) {
params[key] = ctx.getParameter(GL_CONST[key])
}
const debugInfo = ctx.getExtension('WEBGL_debug_renderer_info');
this.glInfo = {
version,
params,
vendor: ctx.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
renderer: ctx.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
}
}
private _deviceInfo() {
let store = Object.create(null);
if (self['_AWAY_DEBUG_STORAGE']) {
try {
store = self['_AWAY_DEBUG_STORAGE'].decodedData();
} catch (e) {
//
}
} else {
try {
const l = localStorage.length;
for(let i = 0; i < l; i ++) {
const k = localStorage.key(i);
store[k] = localStorage.getItem(k);
}
} catch(e) {}
}
return {
agent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
dpi: window.devicePixelRatio
},
store,
memory: performance['memory']
}
}
}