Skip to content

Commit 892d21f

Browse files
committed
Fix solo mode
1 parent 561bb31 commit 892d21f

File tree

8 files changed

+137
-106
lines changed

8 files changed

+137
-106
lines changed

src/components/DoomCanvas/DoomCanvas.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import { EmscriptenModule } from "../../types";
33
import { useAppContext } from "../../context/useAppContext";
44
import { HydraMultiplayer } from "../../utils/hydra-multiplayer";
55
import useKeys from "../../hooks/useKeys";
6+
import { getArgs } from "../../utils/game";
67

78
const DoomCanvas: React.FC = () => {
89
const canvasRef = useRef<HTMLCanvasElement>(null);
910
const isEffectRan = useRef(false);
10-
const { gameData } = useAppContext();
11+
const {
12+
gameData: { code, petName, type },
13+
} = useAppContext();
1114
const keys = useKeys();
1215

1316
useEffect(() => {
@@ -61,20 +64,7 @@ const DoomCanvas: React.FC = () => {
6164
console.log("setStatus:", text);
6265
},
6366
onRuntimeInitialized: function () {
64-
const args = [
65-
"-iwad",
66-
"freedoom2.wad",
67-
"-file",
68-
"Cardano.wad",
69-
"-window",
70-
"-nogui",
71-
"-nomusic",
72-
"-config",
73-
"default.cfg",
74-
...(gameData.code ? ["-connect", "1"] : ["-server", "-deathmatch"]),
75-
...(gameData.petName ? ["-pet", gameData.petName] : []),
76-
];
77-
67+
const args = getArgs({ code, petName, type });
7868
window.callMain(args);
7969
},
8070
};
@@ -91,7 +81,7 @@ const DoomCanvas: React.FC = () => {
9181
canvas.removeEventListener("webglcontextlost", handleContextLost);
9282
document.body.removeChild(script);
9383
};
94-
}, [gameData.code, gameData.petName, keys]);
84+
}, [code, petName, keys, type]);
9585

9686
return <canvas id="canvas" ref={canvasRef} className="w-full h-full" />;
9787
};

src/components/GameView/GameView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const GameView = () => {
5454
<Card className="h-[40rem]">
5555
<DoomCanvas />
5656
</Card>
57-
{gameData.type === "new" && (
57+
{gameData.type === "host" && (
5858
<Card className="px-4 py-2 text-center text-xl text-white flex items-center gap-2 justify-center">
5959
Share this URL with friends{" "}
6060
<a

src/components/InitialView/InitialView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const InitialView: FC<InitialViewProps> = ({ startGame }) => {
4343
...prev,
4444
code: "",
4545
petName: "",
46-
type: undefined,
46+
type: "solo",
4747
}));
4848
};
4949

@@ -55,7 +55,7 @@ const InitialView: FC<InitialViewProps> = ({ startGame }) => {
5555
},
5656
});
5757
setIsNameModalOpen(true);
58-
setGameData((prev) => ({ ...prev, type: "new" }));
58+
setGameData((prev) => ({ ...prev, type: "host" }));
5959
};
6060

6161
const handleClickJoinMultiplayer = () => {

src/context/useAppContext.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
import { createContext, Dispatch, useContext } from "react";
2-
import { GameStatistics, Region } from "../types";
2+
import { GameData, GameStatistics, Region } from "../types";
33
import { UseQueryResult } from "@tanstack/react-query";
44
import { REGIONS } from "../constants";
55

6-
interface GameData {
7-
code: string;
8-
petName: string;
9-
type?: "new" | "join";
10-
}
11-
126
interface AppContextInterface {
137
gameData: GameData;
148
globalQuery?: UseQueryResult<GameStatistics, Error>;
@@ -18,7 +12,7 @@ interface AppContextInterface {
1812
}
1913

2014
export const AppContext = createContext<AppContextInterface>({
21-
gameData: { petName: "", code: "" },
15+
gameData: { petName: "", code: "", type: "solo" },
2216
globalQuery: undefined,
2317
region: REGIONS[0],
2418
setGameData: () => {},

src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,14 @@ export interface EmscriptenModule {
6565
_ReceivePacket?: (from: number, buf: number, len: number) => void;
6666
}
6767

68+
export interface GameData {
69+
code: string;
70+
petName: string;
71+
type: "host" | "join" | "solo";
72+
}
73+
6874
declare global {
6975
interface Window {
70-
// Start the doom game with some set of arguments
7176
callMain: (args: string[]) => void;
7277
Module: EmscriptenModule;
7378
HydraMultiplayer: HydraMultiplayer;

src/utils/game.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { GameData } from "../types";
2+
3+
export const getArgs = ({ type, code, petName }: GameData) => {
4+
const args = [
5+
"-iwad",
6+
"freedoom2.wad",
7+
"-file",
8+
"Cardano.wad",
9+
"-window",
10+
"-nogui",
11+
"-nomusic",
12+
"-config",
13+
"default.cfg",
14+
];
15+
16+
if (type !== "solo") {
17+
if (code) {
18+
args.push("-connect", "1");
19+
} else {
20+
args.push("-server", "-deathmatch");
21+
}
22+
}
23+
24+
if (petName) args.push("-pet", petName);
25+
26+
return args;
27+
};

src/utils/hydra-multiplayer.ts

Lines changed: 83 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Constr, Data, fromHex, toHex, TxComplete, TxHash, UTxO } from "lucid-cardano";
1+
import {
2+
Constr,
3+
Data,
4+
fromHex,
5+
toHex,
6+
TxComplete,
7+
TxHash,
8+
UTxO,
9+
} from "lucid-cardano";
210
import { Hydra } from "./hydra";
311

412
import * as ed25519 from "@noble/ed25519";
@@ -8,103 +16,107 @@ import { Keys } from "../hooks/useKeys";
816
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
917

1018
export class HydraMultiplayer {
11-
keys: Keys;
12-
hydra: Hydra;
13-
myIP: number = 0;
14-
latestUTxO: UTxO | null = null;
15-
packetQueue: Packet[] = [];
16-
17-
constructor(keys: Keys, url: string) {
18-
this.keys = keys;
19-
this.hydra = new Hydra(url, 100);
20-
this.hydra.onTxSeen = this.onTxSeen.bind(this);
21-
22-
this.SendPacket = this.SendPacket.bind(this);
23-
this.setIP = this.setIP.bind(this);
24-
}
19+
keys: Keys;
20+
hydra: Hydra;
21+
myIP: number = 0;
22+
latestUTxO: UTxO | null = null;
23+
packetQueue: Packet[] = [];
24+
25+
constructor(keys: Keys, url: string) {
26+
this.keys = keys;
27+
this.hydra = new Hydra(url, 100);
28+
this.hydra.onTxSeen = this.onTxSeen.bind(this);
29+
30+
this.SendPacket = this.SendPacket.bind(this);
31+
this.setIP = this.setIP.bind(this);
32+
}
2533

26-
public setIP(ip: number) {
27-
this.myIP = ip;
28-
}
34+
public setIP(ip: number) {
35+
this.myIP = ip;
36+
}
2937

30-
public async selectUTxO(): Promise<void> {
31-
if (!!this.latestUTxO) {
32-
return;
33-
}
34-
await this.hydra.populateUTxO();
35-
let utxos = await this.hydra.getUtxos(this.keys.address!);
36-
// TODO: robust
37-
if (this.myIP === 1) {
38-
this.latestUTxO = utxos.find(u => u.outputIndex === 1)!;
39-
} else {
40-
this.latestUTxO = utxos.find(u => u.outputIndex === 0)!;
41-
}
38+
public async selectUTxO(): Promise<void> {
39+
if (this.latestUTxO) {
40+
return;
4241
}
43-
44-
public async SendPacket(to: number, from: number, data: Uint8Array): Promise<void> {
45-
this.packetQueue.push({ to, from, data });
46-
await this.sendPacketQueue();
42+
await this.hydra.populateUTxO();
43+
const utxos = await this.hydra.getUtxos(this.keys.address!);
44+
// TODO: robust
45+
if (this.myIP === 1) {
46+
this.latestUTxO = utxos.find((u) => u.outputIndex === 1)!;
47+
} else {
48+
this.latestUTxO = utxos.find((u) => u.outputIndex === 0)!;
4749
}
50+
}
51+
52+
public async SendPacket(
53+
to: number,
54+
from: number,
55+
data: Uint8Array,
56+
): Promise<void> {
57+
this.packetQueue.push({ to, from, data });
58+
await this.sendPacketQueue();
59+
}
4860

49-
public async sendPacketQueue(): Promise<void> {
50-
if (this.packetQueue.length == 0) {
51-
return;
52-
}
53-
await this.selectUTxO();
54-
let datum = encodePackets(this.packetQueue);
55-
56-
let [newUTxO, tx] = buildTx(this.latestUTxO!, this.keys, datum);
57-
await this.hydra.submitTx(tx);
58-
this.latestUTxO = newUTxO;
59-
this.packetQueue = [];
61+
public async sendPacketQueue(): Promise<void> {
62+
if (this.packetQueue.length == 0) {
63+
return;
6064
}
65+
await this.selectUTxO();
66+
const datum = encodePackets(this.packetQueue);
6167

62-
public onTxSeen(_txId: TxHash, tx: TxComplete): void {
63-
// TODO: tolerate other txs here
64-
const output = tx.txComplete.body().outputs().get(0);
65-
const packetsRaw = output?.datum()?.as_data()?.get().to_bytes();
66-
if (!packetsRaw) {
67-
return;
68-
}
69-
const packets = decodePackets(packetsRaw);
70-
for (const packet of packets) {
71-
if (packet.to == this.myIP) {
72-
let buf = window.Module._malloc!(packet.data.length);
73-
window.Module.HEAPU8!.set(packet.data, buf);
74-
window.Module._ReceivePacket!(packet.from, buf, packet.data.length);
75-
window.Module._free!(buf);
76-
}
77-
}
68+
const [newUTxO, tx] = buildTx(this.latestUTxO!, this.keys, datum);
69+
await this.hydra.submitTx(tx);
70+
this.latestUTxO = newUTxO;
71+
this.packetQueue = [];
72+
}
73+
74+
public onTxSeen(_txId: TxHash, tx: TxComplete): void {
75+
// TODO: tolerate other txs here
76+
const output = tx.txComplete.body().outputs().get(0);
77+
const packetsRaw = output?.datum()?.as_data()?.get().to_bytes();
78+
if (!packetsRaw) {
79+
return;
80+
}
81+
const packets = decodePackets(packetsRaw);
82+
for (const packet of packets) {
83+
if (packet.to == this.myIP) {
84+
const buf = window.Module._malloc!(packet.data.length);
85+
window.Module.HEAPU8!.set(packet.data, buf);
86+
window.Module._ReceivePacket!(packet.from, buf, packet.data.length);
87+
window.Module._free!(buf);
88+
}
7889
}
90+
}
7991
}
8092

8193
interface Packet {
82-
to: number,
83-
from: number,
84-
data: Uint8Array,
94+
to: number;
95+
from: number;
96+
data: Uint8Array;
8597
}
8698

8799
function encodePackets(packets: Packet[]): string {
88100
return Data.to(
89101
packets.map(
90-
({ to, from, data }) => new Constr(0, [BigInt(to), BigInt(from), toHex(data)])
91-
)
102+
({ to, from, data }) =>
103+
new Constr(0, [BigInt(to), BigInt(from), toHex(data)]),
104+
),
92105
);
93106
}
94107

95108
function decodePackets(raw: Uint8Array): Packet[] {
96109
const packets = Data.from(toHex(raw)) as Constr<Data>[];
97110
return packets.map((packet) => {
98-
let [to, from, data] = packet.fields;
111+
const [to, from, data] = packet.fields;
99112
return {
100113
to: Number(to),
101114
from: Number(from),
102115
data: fromHex(data as string),
103-
}
116+
};
104117
});
105118
}
106119

107-
108120
const buildTx = (
109121
inputUtxo: UTxO,
110122
keys: Keys,
@@ -123,9 +135,7 @@ const buildTx = (
123135
`0181a300581d60${keys.publicKeyHashHex!}018200a0028201d818${lengthLengthTag}${datumLengthHex}${datum}` + // Output to users PKH
124136
`0200`; // No fee
125137

126-
const txId = toHex(
127-
blake2b(fromHex(txBodyByHand), { dkLen: 256 / 8 }),
128-
);
138+
const txId = toHex(blake2b(fromHex(txBodyByHand), { dkLen: 256 / 8 }));
129139
const signature = toHex(ed25519.sign(txId, keys.privateKeyBytes!));
130140

131141
const witnessSetByHand = `a10081825820${keys.publicKeyHex!}5840${signature}`; // just signed by the user
@@ -142,4 +152,4 @@ const buildTx = (
142152
};
143153

144154
return [newUtxo, txByHand];
145-
};
155+
};

src/utils/hydra.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class Hydra {
5656
this.utxos = {};
5757
this.tombstones = {};
5858

59-
let websocketUrl = new URL(url);
59+
const websocketUrl = new URL(url);
6060
websocketUrl.protocol = websocketUrl.protocol.replace("http", "ws");
6161
this.connection = new WebSocket(websocketUrl + "?history=no");
6262
this.connection.onopen = () => {
@@ -217,7 +217,7 @@ export class Hydra {
217217
if (this.utxos[txRef]) {
218218
return this.utxos[txRef];
219219
}
220-
let start = performance.now();
220+
const start = performance.now();
221221
await this.populateUTxO();
222222
timeout -= performance.now() - start;
223223
if (timeout <= 0) {
@@ -241,7 +241,12 @@ export class Hydra {
241241
unit: Unit,
242242
): Promise<UTxO[]> {
243243
// we only support address for now
244-
if (!(typeof addressOrCredential === 'string' || addressOrCredential instanceof String)) {
244+
if (
245+
!(
246+
typeof addressOrCredential === "string" ||
247+
addressOrCredential instanceof String
248+
)
249+
) {
245250
throw new Error("not implemented");
246251
}
247252
const ret = [];
@@ -263,7 +268,7 @@ export class Hydra {
263268
throw new Error("UTxO not found");
264269
}
265270
public async getUtxosByOutRef(outRefs: Array<OutRef>): Promise<UTxO[]> {
266-
let ret = [];
271+
const ret = [];
267272
for (const outRef of outRefs) {
268273
const utxo = this.utxos[`${outRef.txHash}#${outRef.outputIndex}`];
269274
if (utxo) {
@@ -310,4 +315,4 @@ function hydraUtxoToLucidUtxo(txHash: TxHash, idx: number, output: any): UTxO {
310315
datum: datumBytes,
311316
assets: assets,
312317
};
313-
}
318+
}

0 commit comments

Comments
 (0)