Get to the point of working proof-of-work.

master
Icedream 2018-04-24 17:11:09 +02:00
parent 9ab0432a5b
commit adfcfb0ae0
Signed by: icedream
GPG Key ID: C1D30A06E6490C14
22 changed files with 435 additions and 78 deletions

View File

@ -103,6 +103,7 @@
"style-loader": "^0.21.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"typeface-changa": "^0.0.56",
"uglifyjs-webpack-plugin": "^1.2.5",
"url-loader": "^1.0.1",
"webfontloader": "^1.6.28",

View File

@ -4,10 +4,16 @@ import { Provider } from 'react-redux';
import store from './redux';
import MessageBoxes from './containers/MessageBoxes';
import ServerTableWrapper from './components/ServerTableWrapper';
import style from './App.styl';
const App = () => (
<Provider store={store}>
<div className={style.app}>
<MessageBoxes />
<ServerTableWrapper />
</div>
</Provider>
);

View File

@ -0,0 +1,5 @@
@import '~typeface-changa'
.app
font-family: Changa, sans-serif
font-size: 16px

View File

@ -0,0 +1,54 @@
import React from 'react';
export default class AnimatedEllipsis extends React.Component {
constructor() {
super();
this.state = {
dots: 0,
};
this.timer = null;
this.componentDidMount = this.componentDidMount.bind(this);
this.componentWillUnmount = this.componentWillUnmount.bind(this);
this.startDotTimer = this.startDotTimer.bind(this);
this.stopDotTimer = this.stopDotTimer.bind(this);
this.incrementDots = this.incrementDots.bind(this);
}
componentDidMount() {
this.startDotTimer();
}
componentWillUnmount() {
this.stopDotTimer();
}
startDotTimer() {
if (this.timer === null) {
this.timer = setInterval(this.incrementDots, 250);
}
}
stopDotTimer() {
if (this.timer !== null) {
clearInterval(this.timer);
this.timer = null;
}
}
incrementDots() {
this.setState(state => ({
dots: (state.dots + 1) % 4,
}));
}
render() {
return (
<span>
{'.'.repeat(this.state.dots)}
</span>
);
}
}

View File

@ -0,0 +1,32 @@
import React from 'react';
import style from './ServerTable.styl';
const ServerTable = ({ servers }) => (
<table className={style.serverTable}>
<thead>
<tr>
<th>Server Name</th>
<th>Variant</th>
<th>Map</th>
<th>Players</th>
<th>Server Address</th>
</tr>
</thead>
<tbody>
{
servers.map(server => (
<tr key={`${server.ip}:${server.port}`}>
<td>{server.name}</td>
<td>{server.variant}<br /><small>{server.variantType}</small></td>
<td>{server.map}</td>
<td>{server.numPlayers} / {server.maxPlayers}</td>
<td>{server.ip}:{server.port}</td>
</tr>
))
}
</tbody>
</table>
);
export default ServerTable;

View File

@ -0,0 +1,2 @@
.serverTable
width: 100%

View File

@ -0,0 +1,27 @@
import React from 'react';
import style from './ServerTableButtons.styl';
const ServerTableButtons = ({
disableRefreshButton,
hideCancelButton,
dispatchRefresh,
dispatchCancelRefresh,
}) => (
<div className={style.serverTableButtons}>
<button
disabled={disableRefreshButton}
onClick={dispatchRefresh}
>
Refresh
</button>
<button
disabled={hideCancelButton}
onClick={dispatchCancelRefresh}
>
Cancel refresh
</button>
</div>
);
export default ServerTableButtons;

View File

@ -0,0 +1,29 @@
import React from 'react';
import * as serverlistLoader from '../redux/serverlist_loader';
import AnimatedEllipsis from './AnimatedEllipsis';
import style from './ServerTable.styl';
const ServerTableStatus = ({ serverCount, state }) => (
<div className={[
style.serverTableStatus,
state === serverlistLoader.states.LOADING
? style.serverTableStatusLoading
: style.serverTableStatusIdle,
].join(' ')}
>
<p>
{state === serverlistLoader.states.LOADING
? <span>Loading<AnimatedEllipsis /></span>
: 'Idle'}
</p>
<p>
{serverCount} servers
</p>
</div>
);
export default ServerTableStatus;

View File

@ -0,0 +1,45 @@
import React from 'react';
import style from './ServerTableWrapper.styl';
import ServerTable from '../containers/ServerTable';
import ServerTableStatus from '../containers/ServerTableStatus';
import ServerTableButtons from '../containers/ServerTableButtons';
export default class ServerTableWrapper extends React.Component {
constructor() {
super();
this.state = {
query: undefined,
queryText: '',
};
this.handleQueryChange = this.handleQueryChange.bind(this);
}
handleQueryChange(e) {
const queryText = e.target.value;
this.setState({
queryText,
});
if (queryText && queryText.length > 0) {
this.setState({
query: item => item.name.toLowerCase().includes(queryText.toLowerCase()),
});
} else {
this.setState({
query: undefined,
});
}
}
render() {
return (
<div className={style.serverTableWrapper}>
<ServerTableStatus />
<ServerTableButtons />
<input type="text" value={this.state.queryText} onChange={this.handleQueryChange} />
<ServerTable filter={this.state.query} />
</div>
);
}
}

View File

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { selectors } from '../redux/serverlist';
import Component from '../components/ServerTable';
const mapStateToProps = (state, { filter }) => {
// TODO - filter
const servers = selectors.getServers(state, filter);
return ({
servers: servers.slice(0, 25),
});
};
const ConnectedMessageBoxes = connect(
mapStateToProps,
)(Component);
export default ConnectedMessageBoxes;

View File

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import * as serverlistLoader from '../redux/serverlist_loader';
import Component from '../components/ServerTableButtons';
const mapStateToProps = state => ({
hideCancelButton: serverlistLoader.selectors.getState(state) === serverlistLoader.states.IDLE,
disableRefreshButton: serverlistLoader.selectors.getState(state) !== serverlistLoader.states.IDLE,
});
const mapDispatchToProps = dispatch => ({
dispatchRefresh: () => {
dispatch(serverlistLoader.actions.updateGameServers());
},
dispatchCancelRefresh: () => {
dispatch(serverlistLoader.actions.cancelUpdateGameServers());
},
});
const ConnectedMessageBoxes = connect(
mapStateToProps,
mapDispatchToProps,
)(Component);
export default ConnectedMessageBoxes;

View File

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import * as serverlist from '../redux/serverlist';
import * as serverlistLoader from '../redux/serverlist_loader';
import Component from '../components/ServerTableStatus';
const mapStateToProps = state => ({
state: serverlistLoader.selectors.getState(state),
serverCount: serverlist.selectors.getServers(state).length,
});
const ConnectedMessageBoxes = connect(
mapStateToProps,
)(Component);
export default ConnectedMessageBoxes;

View File

@ -37,9 +37,9 @@ window.addEventListener('load', () => {
<App />
), rootContainer);
setTimeout(() => {
store.dispatch(serverlist_loader.actions.updateGameServers());
}, 500);
// setTimeout(() => {
// store.dispatch(serverlist_loader.actions.updateGameServers());
// }, 15000);
// setTimeout(() => {
// store.dispatch(ui.actions.add({

View File

@ -1,7 +1,11 @@
import { combineReducers } from 'redux';
// import { combineReducers } from 'redux';
const REDUX_PATH_SEPARATOR = '/';
function normalizeNamespace(name) {
return name.replace(/_(.)/, match => match[1].toUpperCase());
}
export default class ReduxGenerator {
constructor(...id) {
this._id = id;
@ -60,11 +64,22 @@ export default class ReduxGenerator {
.reduce((current, [
name,
{
params,
params = {},
reducer,
},
]) => {
const id = this.actionType(name);
// Validate params, they should all have a validator function as a value
if (params) {
const keysWithNonFunctionValidator = Object.keys(params)
.filter(paramName => typeof (params[paramName]) !== 'function')
.map(v => JSON.stringify(v));
if (keysWithNonFunctionValidator.length > 0) {
throw new Error(`Following params of ${JSON.stringify(id)} have a non-function value as validator: ${keysWithNonFunctionValidator.join(', ')}`);
}
}
const target = {
...current,
actions: {
@ -74,24 +89,24 @@ export default class ReduxGenerator {
const err = Object.entries(payload)
.map(([key, value]) => {
const validator = params[key];
if (!validator) {
if (validator === undefined || validator === null) {
console.warn(`${id}: extra argument ${JSON.stringify(key)}, ignoring`);
return undefined;
}
let result = validator(value);
console.debug(`${id}: verifying ${JSON.stringify(key)} result:`, result);
// console.debug(`${id}: verifying ${JSON.stringify(key)} result:`, result);
switch (typeof result) {
case 'boolean':
case 'boolean': // validator returns either true on success or false on error
if (!result) {
result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`);
} else {
result = null;
}
break;
default:
if (result instanceof Error) {
default: // error or string (or something else?)
if (result instanceof Error) { // modify message to include key
result.message = `${JSON.stringify(key)}: ${result.message}`;
} else {
} else { // use value as part of error reaosn
result = new Error(`${JSON.stringify(key)}: ${result}`);
}
}
@ -122,14 +137,14 @@ export default class ReduxGenerator {
[name]: (state, { type, payload }) => {
// Ignore non-matching actions
if (type !== id) {
console.debug(`${type}: skipping action-mismatching reducer`, { state, payload });
// console.debug(`${type}: skipping action-mismatching reducer`, { state, payload });
return state;
}
// Call original reducer with our payload
console.debug(`${type}: action-matched reducer called`, { state, payload });
// console.debug(`${type}: action-matched reducer called`, { state, payload });
const result = reducer(state, payload);
console.debug(' =>', result);
// console.debug(' =>', result);
return result;
},
};
@ -142,9 +157,9 @@ export default class ReduxGenerator {
reducers: Object.entries(reducers)
.reduce((current, [name, reducer]) => ({
[name]: (state, ...args) => {
console.debug(`${this._id.join('/', `reducer:${name}`)}: called`, { state, args });
// console.debug(`${this._id.join('/', `reducer:${name}`)}: called`, { state, args });
const result = reducer(state, ...args);
console.debug(' =>', result);
// console.debug(' =>', result);
return result;
},
}), {}),
@ -157,23 +172,24 @@ export default class ReduxGenerator {
[name]: (state, ...args) => {
const namespaceState = this._id
.slice(1) // ignore root id
.map(normalizeNamespace)
.reduce(
(currentState, namespaceName) =>
currentState[namespaceName], state);
console.debug(`${[...this._id, `selector:${name}`].join('/')}: called`, { state: namespaceState, args });
// console.debug(`${[...this._id, `selector:${name}`].join('/')}: called`, { state: namespaceState, args });
const result = selector(namespaceState, ...args);
console.debug(' =>', result);
// console.debug(' =>', result);
return result;
},
}), selectors);
retval.reducer = (state, ...args) => {
console.debug(`${this._id.join('/')}: combined reducer called`, { state, args });
// console.debug(`${this._id.join('/')}: combined reducer called`, { state, args });
if (state === undefined) {
console.debug(`${this._id.join('/')}: undefined state passed to reducer`, { state, args });
// console.debug(`${this._id.join('/')}: undefined state passed to reducer`, { state, args });
const result = initialState;
console.debug(' =>', result);
// console.debug(' =>', result);
return result;
}
@ -188,7 +204,7 @@ export default class ReduxGenerator {
);
}
console.debug(`${this._id.join('/')}: no reducers for this namespace, skipping`);
// console.debug(`${this._id.join('/')}: no reducers for this namespace, skipping`);
return state;
};
@ -196,7 +212,7 @@ export default class ReduxGenerator {
// retval.initialState = initialState;
// }
console.debug('generated redux module:', retval);
// console.debug('generated redux module:', retval);
return retval;
}
}

View File

@ -7,7 +7,7 @@ import {
import createSagaMiddleware from 'redux-saga';
import {
all,
fork,
// fork,
} from 'redux-saga/effects';
import * as serverlist from './serverlist';
@ -22,7 +22,7 @@ const reduxModules = Object.entries({
mastersync,
});
console.debug('all redux modules', reduxModules);
// console.debug('all redux modules', reduxModules);
const reducers =
reduxModules
@ -33,7 +33,7 @@ const reducers =
[name]: m.reducer,
};
}, {});
console.debug('combined reducers:', reducers);
// console.debug('combined reducers:', reducers);
const reducer = combineReducers(reducers);
@ -50,10 +50,10 @@ const reducer = combineReducers(reducers);
// create the saga middleware
function* rootSaga() {
const sagas = Object.values(reduxModules)
.reduce((current, [name, m]) => {
const sagas = Object.values(reduxModules) // TODO - !?
.reduce((current, [, m]) => {
if (!m.sagas) {
console.debug(`Module ${name} does not have any sagas`);
// console.debug(`Module ${name} does not have any sagas`);
return current;
}
return [
@ -61,9 +61,9 @@ function* rootSaga() {
...m.sagas,
];
}, []);
console.debug('Will run these sagas from root:', sagas);
// console.debug('Will run these sagas from root:', sagas);
yield all(sagas);
console.debug('Root saga done');
// console.debug('Root saga done');
}
const sagaMiddleware = createSagaMiddleware();

View File

@ -18,7 +18,6 @@ export const states = {
};
function* doDewritoJsonUpdateRequest() {
console.debug('mastersync doDewritoJsonUpdateRequest saga called');
try {
const response = yield call(fetch, 'https://raw.githubusercontent.com/ElDewrito/ElDorito/master/dist/mods/dewrito.json');
if (response.status !== 200) {
@ -33,16 +32,15 @@ function* doDewritoJsonUpdateRequest() {
err,
}));
}
console.debug('mastersync doDewritoJsonUpdateRequest: done');
}
function* storeMasters({
function* storeMasters(x) {
const {
payload: {
err,
dewritoJson,
},
}) {
console.debug('mastersync storeMasters saga called');
} = x;
if (err) {
yield put(ui.actions.add({
text: `Failed to fetch master server information: ${err.message}`,
@ -54,7 +52,6 @@ function* storeMasters({
masterServers,
} = dewritoJson;
yield put(myRedux.actions.setMasters({ masters: masterServers }));
console.debug('mastersync storeMasters: done');
}
export const initialState = {

View File

@ -33,16 +33,16 @@ const myRedux = reduxGenerator.all({
variantType: check.maybe.string,
teamScores: check.maybe.array.of.number,
status: check.string,
numPlayers: check.positiveInteger,
numPlayers: v => check.integer(v) && (check.zero(v) || check.positive(v)),
mods: check.array, // TODO - fine tune this
maxPlayers: check.positiveInteger,
maxPlayers: v => check.integer(v) && (check.zero(v) || check.positive(v)),
passworded: check.boolean,
isDedicated: check.boolean,
gameVersion: check.string,
eldewritoVersion: check.maybe.string,
xnkid: check.maybe.string,
xnaddr: check.maybe.string,
players: check.maybe.array.of.like({
players: v => check.maybe.array.of.like(v, {
name: '',
serviceTag: '',
team: 0,
@ -59,16 +59,19 @@ const myRedux = reduxGenerator.all({
bestStreak: 0,
}),
},
reducer(current, payload) {
// do we have this server on the list already?
if (current.servers.find(server => server.ip === payload.ip)) {
if (current.servers.find(server =>
server.ip === payload.ip
&& server.port === payload.port)) {
// skip this server
return current;
}
// add server
return {
current,
...current,
servers: [
...current.servers,
payload,
@ -77,6 +80,27 @@ const myRedux = reduxGenerator.all({
},
},
addMultiple: {
params: {
servers: v => !!v, // TODO
},
reducer(current, { servers }) {
return { ...current,
servers: servers.reduce((currentServers, server) => {
// is this an actual new server?
if (!currentServers.find(registeredServer =>
server.ip === registeredServer.ip
&& server.port === registeredServer.port)) {
return [...currentServers, server];
}
// server is not new, return old list
return currentServers;
}, current.servers),
};
},
},
reset: {
reducer: () => initialState,
},
@ -86,19 +110,22 @@ const myRedux = reduxGenerator.all({
selectors: {
/*
Example:
filter(state, {
getServers(state, {
name: {
$in: ["substring"],
}
gameType: "slayer",
})
*/
filterServers({ servers }, filter, { order } = {}) {
let filtered = obop.where(servers, filter);
if (order) {
filtered = obop.order(filtered, order);
getServers({ servers }, filter, { order } = {}) {
let finalServers = servers;
if (filter) {
finalServers = obop.where(finalServers, filter);
}
return filtered;
if (order) {
finalServers = obop.order(finalServers, order);
}
return finalServers;
},
},
});

View File

@ -4,8 +4,10 @@ import {
call,
select,
all,
race,
put,
} from 'redux-saga/effects';
import { delay } from 'redux-saga';
import fetch from 'node-fetch';
import check from '../check';
@ -23,6 +25,7 @@ export const states = {
export const initialState = {
state: states.IDLE,
bufferedServers: [],
};
function* inspectGameServer(addr) {
@ -41,21 +44,23 @@ function* inspectGameServer(addr) {
throw new Error(`Unexpected HTTP error code ${response.status}: ${response.statusText}`);
}
const serverInfo = yield call(response.json.bind(response));
check.assert.string(serverInfo.name);
check.assert(check.port(serverInfo.port));
check.assert.maybe.string(serverInfo.hostPlayer);
check.assert.maybe.string(serverInfo.map);
check.assert.maybe.string(serverInfo.mapFile);
check.assert.maybe.string(serverInfo.variant);
check.assert.maybe.string(serverInfo.variantType);
check.assert.integer(serverInfo.maxPlayers);
check.assert.integer(serverInfo.numPlayers);
check.assert.inRange(serverInfo.numPlayers, 0, serverInfo.maxPlayers);
check.assert.array(serverInfo.players);
yield put(serverlist.actions.add({
// check.assert.string(serverInfo.name);
// check.assert(check.port(serverInfo.port));
// check.assert.maybe.string(serverInfo.hostPlayer);
// check.assert.maybe.string(serverInfo.map);
// check.assert.maybe.string(serverInfo.mapFile);
// check.assert.maybe.string(serverInfo.variant);
// check.assert.maybe.string(serverInfo.variantType);
// check.assert.integer(serverInfo.maxPlayers);
// check.assert.integer(serverInfo.numPlayers);
// check.assert.inRange(serverInfo.numPlayers, 0, serverInfo.maxPlayers);
// check.assert.array(serverInfo.players);
yield put(myRedux.actions.updateGameServersBuffer({
server: {
ip,
port,
...serverInfo,
},
}));
} catch (e) {
console.error(`Request to game server ${addr} failed:`, e);
@ -83,7 +88,9 @@ function* updateGameServersFromMaster(masterListUrl) {
if (code !== 0) {
throw new Error(`Server returned error code ${code}: ${msg}`);
}
yield all(servers.map(addr => call(inspectGameServer, addr)));
yield all(servers
// .slice(0, 1) // DEBUG
.map(addr => call(inspectGameServer, addr)));
break;
default:
throw new Error(`Unsupported list version: ${listVersion}`);
@ -94,7 +101,7 @@ function* updateGameServersFromMaster(masterListUrl) {
}
function* updateGameServers() {
yield put(mastersync.actions.updateDewritoJsonStarted());
yield put(myRedux.actions.updateGameServersStarted());
// First let's ensure we have master servers at all
if (!(yield select(mastersync.selectors.hasMasters))) {
@ -102,7 +109,7 @@ function* updateGameServers() {
yield put(mastersync.actions.updateDewritoJson());
const { err } = yield take(mastersync.actionTypes.updateDewritoJsonFinished);
if (err) {
yield put(mastersync.actions.updateDewritoJsonFinished({ err }));
yield put(myRedux.actions.updateGameServersFinished({ err }));
return;
}
}
@ -111,15 +118,38 @@ function* updateGameServers() {
yield put(serverlist.actions.reset());
const masterListUrls = (yield select(mastersync.selectors.getMasters))
.map(server => server.list);
yield all(masterListUrls
.map(url => call(updateGameServersFromMaster, url)));
yield put(mastersync.actions.updateDewritoJsonFinished({}));
yield race([
all(masterListUrls.map(url => call(updateGameServersFromMaster, url))),
take(myRedux.actionTypes.cancelUpdateGameServers),
]);
yield put(myRedux.actions.updateGameServersFinished());
}
function* pushBufferToGameServersList() {
// wait 750 milliseconds or until serverlist checks have all finished
const [, cancel] = yield race([
delay(250),
take(myRedux.actionTypes.updateGameServersBuffer),
take(myRedux.actionTypes.updateGameServersFinished),
]);
if (cancel) return;
const servers = yield select(myRedux.selectors.getBufferedServers);
yield put(myRedux.actions.updateGameServersBuffer({
clear: true,
}));
// console.log(`Would have added ${servers.length} new servers`);
yield put(serverlist.actions.addMultiple({
servers,
}));
}
myRedux = root.sub('serverlist_loader').all({
initialState,
actions: {
cancelUpdateGameServers: {},
updateGameServers: {},
updateGameServersStarted: {
@ -129,6 +159,22 @@ myRedux = root.sub('serverlist_loader').all({
}),
},
updateGameServersBuffer: {
params: {
server: () => true, // TODO
clear: check.maybe.boolean,
},
reducer: (state, { server, clear }) => ({
...state,
bufferedServers: clear
? []
: [
...state.bufferedServers,
server,
],
}),
},
updateGameServersFinished: {
reducer: state => ({
...state,
@ -140,11 +186,13 @@ myRedux = root.sub('serverlist_loader').all({
// selectors are all function(state, ...parameters)
selectors: {
getState: ({ state }) => state,
getBufferedServers: ({ bufferedServers }) => bufferedServers,
},
});
export const sagas = [
takeEvery(myRedux.actionTypes.updateGameServers, updateGameServers),
takeEvery(myRedux.actionTypes.updateGameServersBuffer, pushBufferToGameServersList),
];
export const {

View File

@ -64,8 +64,8 @@ export default (options) => {
.replace(/\[chunkhash(.*?)\]/g, '[hash$1]');
const cssOutputFileName = baseOutputFilename
.replace(/\[ext(.*?)\]/g, 'css')
.replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]');
.replace(/\[ext(.*?)\]/g, 'css');
// .replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]');
// const cssOutputRebasePath = `${slash(path.relative(path.dirname(cssOutputFileName), ''))}/`;
const cssOutputRebasePath = '/';

View File

@ -8383,6 +8383,10 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typeface-changa@^0.0.56:
version "0.0.56"
resolved "https://registry.yarnpkg.com/typeface-changa/-/typeface-changa-0.0.56.tgz#df3ea2e8c9835987917bb0c5aad147be184f5d09"
ua-parser-js@^0.7.9:
version "0.7.17"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"