finally working!

materials
Valerie 2021-07-27 17:48:50 -04:00
parent 327b703201
commit ac510b651e
17 changed files with 381 additions and 160 deletions

View File

@ -61,8 +61,11 @@ export class Game extends Frigid implements Tickable {
this.inventory ??= new Inventory();
this.inventory.validate();
this.clock ??= new Time();
this.clock.thing = this;
this.clock.start();
this.clock.start(this);
this.pawns = [];
if(this.pawns.length === 0) {
for(let i = 0; i < 3; i ++) this.pawns.push(new Pawn());
}
ready(this.name);
}

View File

@ -1,3 +1,4 @@
import chalk from 'chalk';
import EventEmitter from 'events';
import ipc from 'node-ipc';
import {
@ -9,27 +10,31 @@ import {
} from './Constants.js';
let connected = false;
const oldConsoleLog = console.log;
const patchLog = () => console.log = console.log.bind(console, chalk.cyan('[CLIENT]'));
const restoreLog = () => console.log = oldConsoleLog;
// const log = (...args: any[]) => console.log(chalk.cyan('[CLIENT]'), ...args);
class ProcessManagerClass extends EventEmitter {
quit() {
if (connected) {
console.log('client sending quit event')
ipc.of[name].emit(IPC_QUIT_EVENT);
process.exit(0);
} else {
process.exit(0);
}
this.emit('shutdown');
process.exit(0);
}
restart() {
this.emit('shutdown');
if (connected) {
console.log('client emitting ipc restart')
ipc.of[name].emit(IPC_RESTART_EVENT);
process.exit(0);
} else {
console.log('eh?! not connected to tower... closing')
process.exit(0);
}
setTimeout(() => {
process.exit(0);
})
}
get connected() {
return connected;
}
}
@ -40,11 +45,23 @@ ipc.config.appspace = IPC_CLIENT_APPSAPCE;
ipc.config.silent = true;
ipc.connectTo(name, () => {
ipc.of[name].on('connect', () => connected = true);
ipc.of[name].on('disconnect', () => connected = false);
ipc.of[name].on('connect', () => {
connected = true;
patchLog();
});
ipc.of[name].on('disconnect', () => {
connected = false
restoreLog();
});
ipc.of[name].on(IPC_REQUEST_RESTART, () => {
console.log('received restart request');
// ProcessManager.restart();
ProcessManager.emit('reload');
})
});
/////////////
process.on('SIGKILL', () => ProcessManager.quit());
process.on('SIGTERM', () => ProcessManager.quit());
process.on('SIGINT', () => ProcessManager.quit());
///

View File

@ -18,8 +18,9 @@ const months: AbbreviatedMonthName[] = [
'Oct', 'Nov', 'Dec'
]
// TODO split ticker util and calendar util...
export default class Time extends Serializable {
rate: number;
targetTPS: number;
paused = true;
thing: Tickable;
@ -30,6 +31,12 @@ export default class Time extends Serializable {
hour: number;
minute: number;
ticksInSecond: number;
lastTPSCheckpoint: number;
tps: number;
_boundTick: Function;
constructor(timestamp: number = 0) {
super();
this.minute = timestamp;
@ -67,16 +74,34 @@ export default class Time extends Serializable {
}
toString() {
return `${this.hour}:${Math.floor(this.minute).toString().padStart(2, '0')} ${months[this.month]} ${this.day + 1}, ${this.normalizedYear}`
return `${
this.hour
}:${
Math.floor(this.minute).toString().padStart(2, '0')
} ${
months[this.month]
} ${
this.day + 1
}, ${
this.normalizedYear
} [${
this.tps
} / ${
this.targetTPS
}]`;
}
ctor() {
this.rate = 60;
this.targetTPS = 2000;
this.minute ??= 0;
this.hour ??= 0;
this.day ??= 0;
this.month ??= 0;
this.year ??= 0;
this.tps ??= 0;
this.lastTPSCheckpoint = ms4();
this.ticksInSecond = 0;
this._boundTick = this.doTick.bind(this);
}
get second() {
@ -112,7 +137,13 @@ export default class Time extends Serializable {
this.paused = true;
}
start() {
resume() {
this.paused = false;
setTimeout(this.doTick.bind(this), 0);
}
start(tickable: Tickable) {
this.thing = tickable;
this.paused = false;
setTimeout(this.doTick.bind(this), 0);
}
@ -130,8 +161,6 @@ export default class Time extends Serializable {
}
}
normalize() {
// while(t)
while(this.minute >= 60) {
@ -173,20 +202,46 @@ export default class Time extends Serializable {
}
async doTick() {
this.advanceTime(1);
const timeout = 1000 / this.rate;
const start = new Date().getTime();
const timeout = 1000 / this.targetTPS;
// const start = ms4()
const start = ms4();
if(this.thing) {
await this.thing.tick();
}
const elapsed = new Date().getTime() - start;
const wait = Math.max(timeout - elapsed, 0);
const end = ms4()
const elapsed = end - start;
const wait = timeout - elapsed;
const normalizedWait = Math.floor(Math.max(wait, 0));
// process.stdout.write(`tick took ${elapsed} waiting ${normalizedWait}\n`);
if(wait < 0) {
const ticksOver = (-wait / timeout) + 1;
console.log(chalk.yellow('Can\'t keep up! Tick took ' + ticksOver.toFixed(2) + ' ticks (' + (timeout - wait).toFixed(4) + 'ms)'));
}
this.ticksInSecond ++;
if(end > this.lastTPSCheckpoint + 1000) {
this.lastTPSCheckpoint = end;
this.tps = this.ticksInSecond;
this.ticksInSecond = 0;
}
if(this.paused) return;
setTimeout(this.doTick.bind(this), wait)
setTimeout(this._boundTick, normalizedWait)
}
}
export interface Tickable {
tick: () => Promise<void>
}
function ms4() {
const a = process.hrtime()
return a[0]*10e2 + a[1]/1000000;
}

View File

@ -3,6 +3,8 @@ import { parse, resolve } from 'path';
import walkSync from 'walk-sync';
import { fileURLToPath } from 'url';
import { APPLICATION_NAME } from './Constants.js';
import chalk from 'chalk';
export function osrsNumber(x: number): string {
if(x < 10_000) return '' + x;
@ -11,7 +13,7 @@ export function osrsNumber(x: number): string {
}
export async function loadExtensions() {
console.log(APPLICATION_NAME + ': Loading extensions');
console.log('Loading extensions');
const extensionsPath = resolve(parse(fileURLToPath(import.meta.url)).dir, '../content');
const extensions = walkSync(extensionsPath)
@ -28,6 +30,4 @@ export async function loadExtensions() {
}
console.log('Setup Complete.');
}
// export function
}

View File

@ -6,83 +6,85 @@ import {
IPC_REQUEST_RESTART
} from './Constants.js';
import { spawn, ChildProcess } from 'child_process';
import watch from 'watch';
import chokidar from 'chokidar';
import chalk from 'chalk';
ipc.config.silent = true;
// ipc.config.silent = true;
const exec = 'qode' + (process.platform === "win32" ? '.cmd' : '');
// should be obtained from process spawn args, but whatever!
const exec = 'qode' +
(process.platform === "win32" ? '.cmd' : '');
const args = [
'bin/app.bundle.cjs'
]
];
ipc.serve(IPC_PATH, () => {
ipc.server.on(IPC_QUIT_EVENT, async () => {
await killProcess();
ipc.server.stop();
process.exit(0);
});
ipc.server.on(IPC_RESTART_EVENT, restart)
});
console.log('started ipc tower server!')
ipc.server.start();
const log = console.log.bind(console, chalk.green('[TOWER]'));
// varying state data
let connected = 0;
let proc: ChildProcess = null;
let restartTimer: NodeJS.Timeout = null;
function ensureAlive() {
if(proc) {
return;
}
function startProcess() {
proc = spawn(exec, args, {
stdio: 'inherit'
});
console.log(`[${proc.pid}] ${chalk.grey(`${exec} ${args.join(' ')}`)}`);
proc.on('exit', () => {
console.log('process died');
proc.once('exit', () => {
proc = null;
})
});
log(`[${
proc.pid
}] ${
chalk.grey(`${
exec
} ${
args.join(' ')
}`)
}`);
}
async function killProcess() {
if(proc) {
console.log('killing process...');
const killedPromise = new Promise((res) => {
proc.on('exit', (code, sig) => {
res(code || sig);
})
})
proc.kill();
console.log('process died with code', await killedPromise);
proc = null;
console.log()
async function ensureDead() {
if(!proc) {
return;
}
const killedPromise =
new Promise(res => proc.once('exit', res));
proc.kill(9);
await killedPromise;
proc = null;
}
async function restart() {
console.log('received restart event');
await killProcess();
console.log('')
startProcess();
await ensureDead();
ensureAlive();
}
startProcess();
let restartTimer: NodeJS.Timeout = null;
function fileChange() {
// appendFileSync('log.log', evt + ' ' + path + '\n');
// console.log(cluster.isMaster, evt, path);
if(restartTimer) clearTimeout(restartTimer)
restartTimer = setTimeout(() => {
console.log('changes detected');
if(proc) {
ipc.server.broadcast(IPC_REQUEST_RESTART);
} else {
startProcess();
}
ensureAlive();
ipc.server.broadcast(IPC_REQUEST_RESTART);
restartTimer = null;
}, 100);
}
chokidar.watch('./out').on('all', fileChange);
// watch.watchTree('./bin', fileChange);
// start the server, connect events
ipc.serve(IPC_PATH, () => {
ipc.server.on(IPC_QUIT_EVENT, ensureDead);
ipc.server.on(IPC_RESTART_EVENT, restart);
ipc.server.on('connect', () => connected ++);
ipc.server.on('disconnect', () => connected --);
});
ipc.server.start();
// open the process
ensureAlive();
//begin watching for files, ignore changes on boot.
chokidar.watch('./out', {
ignoreInitial: true
}).on('all', fileChange);

View File

@ -16,25 +16,25 @@ import ansi from 'sisteransi';
import './../content/content.js';
import { loadExtensions } from './Util.js';
import { APPLICATION_NAME } from './Constants.js';
import chalk from 'chalk';
import { ProcessManager } from './ProcessManager.js';
// console.clear();
function gracefulShutdown() {
if (isStarted()) {
stop();
}
console.log('shutting down gracefully...');
if (Game.current) {
console.log('saving world...');
Game.current.sync();
}
console.log('exitting');
process.stdout.write(ansi.cursor.show);
ProcessManager.on('shutdown', gracefulShutdown);
process.exit(0);
function gracefulShutdown() {
// if (isStarted()) {
// stop();
// }
if (Game.current) {
console.log(chalk.cyan('Saving world...'));
Game.current.sync();
console.log(chalk.cyan('World Saved!'));
}
}
process.on('exit', gracefulShutdown);
// process.on('exit', gracefulShutdown);
const saveFile = process.argv[2] || 'data/world01.json';
@ -42,10 +42,11 @@ ensureDirSync(parse(saveFile).dir);
// loadExtensions();
for (let seconds = 0; seconds > 0; seconds --) {
process.stdout.write('Starting ' + APPLICATION_NAME + ' in ' + seconds + '\r');
}
process.stdout.write('Starting ' + APPLICATION_NAME + ' in ' + 0 + '\n');
// TODO replace with splash screen
// for (let seconds = 0; seconds > 0; seconds --) {
// process.stdout.write('Starting ' + APPLICATION_NAME + ' in ' + seconds + '\r');
// }
// process.stdout.write('Starting ' + APPLICATION_NAME + ' in ' + 0 + '\n');
// console.clear();
// TODO move render logic into game, so that the ui doesnt exist until the game does...

View File

@ -5,7 +5,7 @@ import * as uuid from 'uuid';
import faker from 'faker';
import chalk from 'chalk';
import { Item } from '../registries/Items.js';
import WebSocket from 'ws';
import WebSocket, { EventEmitter } from 'ws';
import { Popup } from '@ui';
import { inspect } from 'util'
import { Pawn } from '../Pawn.js';
@ -18,11 +18,11 @@ const mdns = bonjour();
const ID = uuid.v4();
let devices: Player[] = [];
const network = {
const network = new (class Network extends EventEmitter {
get players() {
return devices;
}
}
})();
export type GiftMessage = {
pawns: string[],
@ -61,11 +61,13 @@ export async function ready(name: string) {
mdns.find({
type: MDNS_TYPE
}, (service) => {
network.emit('change');
const p = new Player();
p.name = service.name;
p.host = service.host;
p.port = service.port;
devices.push(p);
}).on("down", (service) => {
network.emit('change');
// TODO remove player from MP
})

View File

@ -1,18 +1,19 @@
import { Game } from '@game';
import { ItemState } from '@items';
import {
QLabel,
QTabWidget,
QWidget,
QIcon,
FlexLayout,
QGridLayout,
FocusPolicy,
WidgetEventTypes,
AlignmentFlag,
QListView,
QListWidget,
QListWidgetItem
QBoxLayout,
Direction,
QScrollArea,
} from '@nodegui/nodegui';
import network from '../multiplayer/mDNS.js';
import { Player } from '../multiplayer/Player.js';
import { Pawn } from '../Pawn.js';
import { View } from './View.js';
@ -33,9 +34,6 @@ export class GameView extends View {
this.title.setText(this.game.name);
this.left.setInlineStyle('border: 1px solid white;')
this.right.setInlineStyle('border: 1px solid white;')
this.layout.addWidget(this.title, 0, 0);
this.layout.addWidget(this.timeLabel, 0, 1);
this.layout.addWidget(this.left, 1, 0);
@ -46,7 +44,8 @@ export class GameView extends View {
this.layout.setColumnStretch(1, 2);
this.right.addTab(new PawnPageWidget(), new QIcon(), 'Pawns');
this.right.addTab(new InventoryWidget(), new QIcon(), 'Inventory')
this.right.addTab(new InventoryPageWidget(), new QIcon(), 'Inventory');
this.right.addTab(new MultiplayerPageWidget(), new QIcon(), 'Multiplayer');
}
constructor(game: Game) {
@ -55,52 +54,154 @@ export class GameView extends View {
}
}
class PawnWidget extends QWidget {
constructor(pawn: Pawn) {
class GridItem extends QWidget {
rootLayout: QGridLayout;
get layout(): QGridLayout {
return this.rootLayout;
}
constructor() {
super();
let layout: QGridLayout;
this.setLayout(layout = new QGridLayout());
// this.setInlineStyle(`
// margin-bottom: 4px;
// `);
const nameLabel = new QLabel();
nameLabel.setText(pawn.name.first + ' ' + pawn.name.last);
nameLabel.setAlignment(AlignmentFlag.AlignLeft | AlignmentFlag.AlignTop);
const activityLabel = new QLabel();
activityLabel.setText(pawn.status);
activityLabel.setAlignment(AlignmentFlag.AlignRight | AlignmentFlag.AlignTop);
this.layout.addWidget(nameLabel, 0, 0, 1, 1);
this.layout.addWidget(activityLabel, 0, 1, 1, 1);
layout.setColumnStretch(0, 1);
layout.setColumnStretch(1, 1);
layout.setRowStretch(0, 1);
// this.setFocusPolicy(FocusPolicy.ClickFocus);
this.rootLayout = new QGridLayout()
this.setLayout(this.rootLayout);
this.setInlineStyle(`
width: \'100%\';
background: coral;
margin: 0px;
padding: 0px;
`);
this.setFocusPolicy(FocusPolicy.ClickFocus);
}
}
class PawnPageWidget extends QListWidget {
function addSplitText(layout: QGridLayout, left: string, right: string, row: number) {
layout.setSpacing(0);
const nameLabel = new QLabel();
nameLabel.setText(left);
nameLabel.setAlignment(AlignmentFlag.AlignLeft | AlignmentFlag.AlignTop);
const activityLabel = new QLabel();
activityLabel.setText(right);
activityLabel.setAlignment(AlignmentFlag.AlignRight | AlignmentFlag.AlignTop);
layout.addWidget(nameLabel, row, 0, 1, 1);
layout.addWidget(activityLabel, row, 1, 1, 1);
layout.setRowStretch(row, 1);
// in theory this is redundant, calling this
// function on the same layout multiple times...
layout.setColumnStretch(0, 1);
layout.setColumnStretch(1, 1);
}
class PawnWidget extends GridItem {
constructor(pawn: Pawn) {
super();
addSplitText(
this.layout,
pawn.name.first + ' ' + pawn.name.last,
pawn.status,
0
);
}
}
class ItemWidget extends GridItem {
constructor(itemState: ItemState<unknown>) {
super();
addSplitText(
this.layout,
itemState.name,
'' + (itemState.qty),
0
);
}
}
abstract class ScrollPanel extends QScrollArea {
centralWidget: QWidget;
vLayout: QBoxLayout;
constructor() {
super();
// this.setLayout(new FlexLayout());
this.setInlineStyle('background: purple;');
// this.layout.addWidget(new PawnWidget(Game.current.pawns[0]));
this.setInlineStyle(`
background: rgba(0, 0, 0, 0);
border: none;
`)
this.centralWidget = new QWidget();
this.centralWidget.setInlineStyle(`
background: rgba(0, 0, 0, 0);
`)
// this.setVerticalScrollBarPolicy(ScrollBarPolicy.ScrollBarAlwaysOn);
this.setWidgetResizable(true);
this.setWidget(this.centralWidget);
this.vLayout = new QBoxLayout(Direction.TopToBottom);
this.centralWidget.setLayout(this.vLayout);
this.fill();
this.vLayout.addStretch(1);
// for(let i = 0; i < 100; i ++) {
// const button = new QPushButton();
// button.setText('' + i);
// this.vLayout.addWidget(button);
// }
}
refill() {
for(const a of this.nodeChildren) {
console.log(a);
}
this.fill();
}
addWidget(widget: QWidget) {
this.vLayout.addWidget(widget);
}
abstract fill(): void;
}
class PawnPageWidget extends ScrollPanel {
fill() {
for(const pawn of Game.current.pawns) {
// const label = new QLabel();
// label.setText(pawn.name.first);
// this.layout.addWidget(label);
// this.layout.addWidget(new PawnWidget(pawn));
const item = new QListWidgetItem();
this.addItem(item);
this.setItemWidget(item, new PawnWidget(pawn));
this.addWidget(new PawnWidget(pawn));
}
}
}
// class PawnPageWidget extends QListWidget {
// constructor() {
// super();
// for (const pawn of Game.current.pawns) {
// const pawnWidget = new PawnWidget(pawn);
// const item = new QListWidgetItem();
// this.addItem(item);
// this.setItemWidget(item, pawnWidget);
// }
// }
// }
// class PawnPageWidget extends QWidget {
// constructor() {
// super();
// this.setLayout(new QBoxLayout(Direction.TopToBottom));
// // this.setLayout(new FlexLayout());
// for(const pawn of Game.current.pawns) {
// this.layout.addWidget(new PawnWidget(pawn));
// }
// }
// }
class TimeWidget extends QLabel {
constructor() {
@ -112,6 +213,38 @@ class TimeWidget extends QLabel {
}
}
class InventoryWidget extends QWidget {
class InventoryPageWidget extends ScrollPanel {
fill() {
for(const itemState of Game.current.inv.items) {
this.addWidget(new ItemWidget(itemState))
}
}
}
class MultiplayerPlayerWidget extends GridItem {
constructor(player: Player) {
super();
addSplitText(
this.layout,
player.name,
player.host + ':' + player.port,
0
)
}
}
class MultiplayerPageWidget extends ScrollPanel {
constructor() {
super();
network.on('change', () => {
this.refill();
})
}
fill(): void {
for(const player of network.players) {
this.addWidget(new MultiplayerPlayerWidget(player))
}
}
}

View File

@ -20,10 +20,14 @@ win.setWindowTitle(APPLICATION_NAME);
win.show();
(global as any).win = win;
win.addEventListener(WidgetEventTypes.Paint, _ => _);
win.addEventListener('customContextMenuRequested', console.log)
win.addEventListener('objectNameChanged', console.log)
win.addEventListener('windowIconChanged', console.log)
win.addEventListener('windowTitleChanged', console.log)
win.addEventListener('customContextMenuRequested', console.log);
win.addEventListener('objectNameChanged', console.log);
win.addEventListener('windowIconChanged', console.log);
win.addEventListener('windowTitleChanged', console.log);
win.addEventListener(
WidgetEventTypes.Close,
() => ProcessManager.quit()
);
setView(new LoadingView());
@ -54,7 +58,6 @@ export function isStarted() {
ProcessManager.on('reload', () => {
RequestReloadPopup.show();
//
});
@ -62,6 +65,6 @@ ProcessManager.on('reload', () => {
function f() {
win.repaint();
win.update();
setTimeout(f, 100);
setTimeout(f, 0);
}
f();

View File

@ -36,7 +36,7 @@ export class Item<Data = any> {
register(force = true) {
if((!this.id || !this.name) && !force) return;
console.log('Added item', (this.name.singular ?? "[No Name]").padStart(20, ' '), `| (${this.id})`)
// console.log('Added item', (this.name.singular ?? "[No Name]").padStart(20, ' '), `| (${this.id})`)
items.set(this.id, this);
return this;
}
@ -63,6 +63,10 @@ export class ItemState<Data> extends Serializable {
return new ItemState<Data>(this.item, qty, this.data);
}
get name() {
return this.qty === 1 ? this.item.name.singular : this.item.name.plural;
}
get item() {
if(!items.has(this.itemId))
throw new Error('unknown item: ' + this.itemId);

View File

@ -80,7 +80,7 @@ let currentTheme = backupTheme;
const themes: Map<ThemeName, Theme> = new Map();
export function registerTheme(name: ThemeName, theme: Partial<Theme>) {
console.log('Registered theme', name);
// console.log('Registered theme', name);
themes.set(name, merge(backupTheme, theme));
}

View File

@ -40,7 +40,7 @@ export class EscapeMenu {
} else if (key.full === 'enter') {
switch(this.selected) {
case 0: {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
break;
}
@ -54,7 +54,7 @@ export class EscapeMenu {
}
}
} else if(key.full === 'escape') {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
}
this.render();

View File

@ -29,7 +29,7 @@ export class GiftPopup {
if(key.full === 'enter') {
this.send();
} if(key.full === 'escape' || key.full === 'enter') {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
} else if (key.full === 'up') {
this.selected --;

View File

@ -23,7 +23,7 @@ export class PawnDetails {
});
this.box.on('keypress', (evt: {}, key: {full: string}) => {
if(key.full === 'escape' || key.full === 'enter') {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
} else if (key.full === 'up') {
// this.selected --;

View File

@ -23,7 +23,7 @@ export class Popup {
});
this.box.on('keypress', (evt: {}, key: {full: string}) => {
if(key.full === 'escape' || key.full === 'enter') {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
}
});

View File

@ -33,7 +33,7 @@ export class SelectItem {
}
private close() {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
}

View File

@ -122,6 +122,7 @@ export function start() {
export function stop() {
screen.destroy();
// process.stdout.write('\x1b[?1049l');
process.stdout.write(ansi.cursor.show);
}
// move to some debugging shit, idk