Skip to content

Commit 730f3ad

Browse files
committed
feat: moving game event subscription to grid page
1 parent 9ec88ec commit 730f3ad

File tree

2 files changed

+232
-168
lines changed

2 files changed

+232
-168
lines changed

electron/renderer/pages/grid.tsx

+176-59
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dynamic from 'next/dynamic';
2+
import { useObservable, useSubscription } from 'observable-hooks';
23
import type { ReactNode } from 'react';
3-
import { createElement, isValidElement, useEffect, useState } from 'react';
4+
import { useCallback, useEffect, useState } from 'react';
45
import * as rxjs from 'rxjs';
56
import { Grid } from '../components/grid';
67
import { useLogger } from '../components/logger';
@@ -10,110 +11,226 @@ import { useLogger } from '../components/logger';
1011
// https://nextjs.org/docs/messages/react-hydration-error
1112
const GridNoSSR = dynamic(async () => Grid, { ssr: false });
1213

13-
interface DougProps {
14-
stream$: rxjs.Observable<ReactNode>;
14+
interface DougCmpProps {
15+
stream$: rxjs.Observable<{ streamId: string; text: string }>;
1516
}
1617

17-
const DougCmp: React.FC<DougProps> = (props: DougProps): ReactNode => {
18+
const DougCmp: React.FC<DougCmpProps> = (props: DougCmpProps): ReactNode => {
1819
const { stream$ } = props;
1920

20-
const [lines, setLines] = useState<Array<string>>([]);
21+
useSubscription(stream$, (stream) => {
22+
if (stream.text === '__CLEAR_STREAM__') {
23+
setGameText([]);
24+
} else {
25+
appendGameText(stream.text);
26+
}
27+
});
28+
29+
const [gameText, setGameText] = useState<Array<string>>([]);
30+
31+
const appendGameText = useCallback((newText: string) => {
32+
const scrollbackBuffer = 500; // max number of most recent lines to keep
33+
newText = newText.replace(/\n/g, '<br/>');
34+
setGameText((oldTexts) => {
35+
return oldTexts.concat(newText).slice(scrollbackBuffer * -1);
36+
});
37+
}, []);
38+
39+
return (
40+
<div>
41+
{gameText.map((text, index) => {
42+
return (
43+
<span key={index} style={{ fontFamily: 'Verdana' }}>
44+
<span dangerouslySetInnerHTML={{ __html: text }} />
45+
</span>
46+
);
47+
})}
48+
</div>
49+
);
50+
};
51+
52+
// I started tracking this via `useState` but when calling it's setter
53+
// the value did not update fast enough before a text game event
54+
// was received, resulting in text routing to the wrong stream window.
55+
let gameStreamId = '';
56+
57+
const GridPage: React.FC = (): ReactNode => {
58+
const { logger } = useLogger('page:grid');
59+
60+
// Game events by subscribing to the game event IPC channel.
61+
// Are routed to the correct game stream window via `gameStreamSubject$`.
62+
const gameEventsSubject$ = useObservable(() => {
63+
return new rxjs.Subject<{ type: string } & Record<string, any>>();
64+
});
65+
66+
// Content destined for a specific game stream window.
67+
// For example, 'room' or 'combat'.
68+
const gameStreamSubject$ = useObservable(() => {
69+
return new rxjs.Subject<{ streamId: string; text: string }>();
70+
});
71+
72+
// Track high level game events such as stream ids and formatting.
73+
// Re-emit text events to the game stream subject to get to grid items.
74+
useSubscription(gameEventsSubject$, (gameEvent) => {
75+
switch (gameEvent.type) {
76+
case 'TEXT':
77+
gameStreamSubject$.next({
78+
streamId: gameStreamId,
79+
text: gameEvent.text,
80+
});
81+
break;
82+
case 'CLEAR_STREAM':
83+
gameStreamSubject$.next({
84+
streamId: gameEvent.streamId,
85+
text: '__CLEAR_STREAM__',
86+
});
87+
break;
88+
case 'PUSH_STREAM':
89+
gameStreamId = gameEvent.streamId;
90+
break;
91+
case 'POP_STREAM':
92+
gameStreamId = '';
93+
break;
94+
}
95+
});
2196

2297
useEffect(() => {
23-
console.log('subscribing to stream');
24-
const subscription = stream$.subscribe((element) => {
25-
if (element) {
26-
if (isValidElement(element)) {
27-
setLines((lines) => [...lines, element.props.children]);
28-
}
98+
window.api.onMessage(
99+
'game:connect',
100+
(_event, { accountName, characterName, gameCode }) => {
101+
logger.info('game:connect', { accountName, characterName, gameCode });
29102
}
103+
);
104+
105+
return () => {
106+
window.api.removeAllListeners('game:connect');
107+
};
108+
}, [logger]);
109+
110+
useEffect(() => {
111+
window.api.onMessage(
112+
'game:disconnect',
113+
(_event, { accountName, characterName, gameCode }) => {
114+
logger.info('game:disconnect', {
115+
accountName,
116+
characterName,
117+
gameCode,
118+
});
119+
}
120+
);
121+
122+
return () => {
123+
window.api.removeAllListeners('game:disconnect');
124+
};
125+
}, [logger]);
126+
127+
useEffect(() => {
128+
window.api.onMessage('game:error', (_event, error: Error) => {
129+
logger.error('game:error', { error });
30130
});
31131

32132
return () => {
33-
console.log('unmounting');
34-
subscription.unsubscribe();
133+
window.api.removeAllListeners('game:error');
35134
};
36-
}, [stream$]);
135+
}, [logger]);
37136

38-
const output = lines.map((line, index) => {
39-
return <p key={index}>{line}</p>;
40-
});
41-
console.log('rendering lines', { output });
137+
useEffect(() => {
138+
window.api.onMessage('game:event', (_event, gameEvent) => {
139+
logger.debug('game:event', { gameEvent });
140+
gameEventsSubject$.next(gameEvent);
141+
});
42142

43-
return <div>{output}</div>;
44-
};
143+
return () => {
144+
window.api.removeAllListeners('game:event');
145+
};
146+
}, [logger, gameEventsSubject$]);
45147

46-
const GridPage: React.FC = (): ReactNode => {
47-
const { logger } = useLogger('page:grid');
148+
// TODO the list of items we inject should come from user preferences
149+
// if none then provide our own default list
150+
// TODO users should be able to add/remove items from the grid
151+
// we already support closing grid items, but not synced to prefs yet
48152

49-
// TODO get a filtered game-stream of only <pushStream> tags
50-
// TODO load grid layout from storage (the 'i' property is the key)
51-
// TODO load game-window key values from storage (e.g. { id: 'percWindow', label: 'Spells' })
52-
// a lot of these we know, but DR may introduce others in the future
53-
// so there will be our default data plus user-defined data
54-
55-
const gamePushStream$ = rxjs.interval(1000).pipe(
56-
rxjs.take(10),
57-
rxjs.map((i) => {
58-
if (i % 2 === 0) {
59-
return createElement(
60-
'pushStream',
61-
{ id: 'room' },
62-
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae elit libero, a pharetra augue.'
63-
);
64-
}
65-
return createElement(
66-
'pushStream',
67-
{ id: 'percWindow' },
68-
'Ethereal Shield (5 roisean)'
69-
);
70-
})
71-
);
153+
// TODO subscribe to game events and route them to the correct grid item
72154

73155
return (
74156
<GridNoSSR
75157
items={[
76158
{
77159
itemId: 'room',
78160
title: 'Room',
79-
// content: <DougCmp stream$={gamePushStream$} />,
80-
content: <div>empty</div>,
161+
content: (
162+
<DougCmp
163+
stream$={gameStreamSubject$.pipe(
164+
rxjs.filter((m) => m.streamId === 'room')
165+
)}
166+
/>
167+
),
81168
},
82169
{
83170
itemId: 'percWindow',
84171
title: 'Spells',
85-
// content: <DougCmp stream$={gamePushStream$} />,
86-
content: <div>empty</div>,
172+
content: (
173+
<DougCmp
174+
stream$={gameStreamSubject$.pipe(
175+
rxjs.filter((m) => m.streamId === 'percWindow')
176+
)}
177+
/>
178+
),
87179
},
88180
{
89181
itemId: 'inv',
90182
title: 'Inventory',
91-
// content: <DougCmp stream$={gamePushStream$} />,
92-
content: <div>empty</div>,
183+
content: (
184+
<DougCmp
185+
stream$={gameStreamSubject$.pipe(
186+
rxjs.filter((m) => m.streamId === 'inv')
187+
)}
188+
/>
189+
),
93190
},
94191
{
95192
itemId: 'familiar',
96193
title: 'Familiar',
97-
// content: <DougCmp stream$={gamePushStream$} />,
98-
content: <div>empty</div>,
194+
content: (
195+
<DougCmp
196+
stream$={gameStreamSubject$.pipe(
197+
rxjs.filter((m) => m.streamId === 'familiar')
198+
)}
199+
/>
200+
),
99201
},
100202
{
101203
itemId: 'thoughts',
102204
title: 'Thoughts',
103-
// content: <DougCmp stream$={gamePushStream$} />,
104-
content: <div>empty</div>,
205+
content: (
206+
<DougCmp
207+
stream$={gameStreamSubject$.pipe(
208+
rxjs.filter((m) => m.streamId === 'thoughts')
209+
)}
210+
/>
211+
),
105212
},
106213
{
107214
itemId: 'combat',
108215
title: 'Combat',
109-
// content: <DougCmp stream$={gamePushStream$} />,
110-
content: <div>empty</div>,
216+
content: (
217+
<DougCmp
218+
stream$={gameStreamSubject$.pipe(
219+
rxjs.filter((m) => m.streamId === 'combat')
220+
)}
221+
/>
222+
),
111223
},
112224
{
113225
itemId: 'main',
114226
title: 'Main',
115-
// content: <DougCmp stream$={gamePushStream$} />,
116-
content: <div>empty</div>,
227+
content: (
228+
<DougCmp
229+
stream$={gameStreamSubject$.pipe(
230+
rxjs.filter((m) => m.streamId === '')
231+
)}
232+
/>
233+
),
117234
},
118235
]}
119236
/>

0 commit comments

Comments
 (0)