Skip to content

Commit 253efc3

Browse files
committed
add multi game support
1 parent 86fb296 commit 253efc3

File tree

17 files changed

+389
-36
lines changed

17 files changed

+389
-36
lines changed

web/public/deadlock.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

web/public/dota2.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React, { useContext } from "react";
2+
import { Link } from "react-router-dom";
3+
import styled from "styled-components";
4+
import { Declaration, SchemaClass, SchemaEnum } from "./api";
5+
import { DeclarationsContext } from "./DeclarationsContext";
6+
import { GAMES, GameId } from "~games";
7+
8+
const Wrapper = styled.div`
9+
padding: 10px 14px;
10+
border-top: 1px solid ${(props) => props.theme.groupSeparator};
11+
`;
12+
13+
const Title = styled.div`
14+
font-size: 14px;
15+
font-weight: 600;
16+
text-transform: uppercase;
17+
letter-spacing: 0.04em;
18+
color: ${(props) => props.theme.textDim};
19+
margin-bottom: 6px;
20+
`;
21+
22+
const RefList = styled.div`
23+
display: flex;
24+
flex-wrap: wrap;
25+
gap: 5px;
26+
`;
27+
28+
const GameLink = styled(Link)<{ $status: "identical" | "differs" }>`
29+
display: inline-flex;
30+
align-items: center;
31+
gap: 6px;
32+
padding: 3px 10px;
33+
border-radius: 6px;
34+
font-size: 14px;
35+
text-decoration: none;
36+
color: ${(props) => props.theme.text};
37+
background: ${(props) => props.theme.groupMembers};
38+
border: 1px solid ${(props) => props.$status === "identical" ? props.theme.groupBorder : "#c97a1e"};
39+
transition: border-color 0.1s;
40+
41+
&:hover {
42+
border-color: ${(props) => props.theme.highlight};
43+
}
44+
`;
45+
46+
const StatusDot = styled.span<{ $status: "identical" | "differs" }>`
47+
width: 8px;
48+
height: 8px;
49+
border-radius: 50%;
50+
background-color: ${(props) => props.$status === "identical" ? "#4a8c2a" : "#c97a1e"};
51+
flex-shrink: 0;
52+
`;
53+
54+
function areClassesEqual(a: SchemaClass, b: SchemaClass): boolean {
55+
if (a.parents.length !== b.parents.length) return false;
56+
for (let i = 0; i < a.parents.length; i++) {
57+
if (a.parents[i].name !== b.parents[i].name || a.parents[i].module !== b.parents[i].module) return false;
58+
}
59+
if (a.fields.length !== b.fields.length) return false;
60+
for (let i = 0; i < a.fields.length; i++) {
61+
if (a.fields[i].name !== b.fields[i].name || a.fields[i].offset !== b.fields[i].offset) return false;
62+
if (JSON.stringify(a.fields[i].type) !== JSON.stringify(b.fields[i].type)) return false;
63+
}
64+
if (JSON.stringify(a.metadata) !== JSON.stringify(b.metadata)) return false;
65+
return true;
66+
}
67+
68+
function areEnumsEqual(a: SchemaEnum, b: SchemaEnum): boolean {
69+
if (a.alignment !== b.alignment) return false;
70+
if (a.members.length !== b.members.length) return false;
71+
for (let i = 0; i < a.members.length; i++) {
72+
if (a.members[i].name !== b.members[i].name || a.members[i].value !== b.members[i].value) return false;
73+
}
74+
if (JSON.stringify(a.metadata) !== JSON.stringify(b.metadata)) return false;
75+
return true;
76+
}
77+
78+
function areDeclarationsEqual(a: Declaration, b: Declaration): boolean {
79+
if (a.kind !== b.kind) return false;
80+
if (a.kind === "class" && b.kind === "class") return areClassesEqual(a, b);
81+
if (a.kind === "enum" && b.kind === "enum") return areEnumsEqual(a, b);
82+
return false;
83+
}
84+
85+
export function CrossGameRefs({ name, module, kind }: { name: string; module: string; kind: "class" | "enum" }) {
86+
const { game, allGames, declarations } = useContext(DeclarationsContext);
87+
if (!allGames) return null;
88+
89+
const current = declarations.find((d) => d.name === name && d.module === module && d.kind === kind);
90+
if (!current) return null;
91+
92+
const otherGames: { gameId: GameId; gameName: string; status: "identical" | "differs"; module: string }[] = [];
93+
94+
for (const [gameId, gameDeclarations] of allGames) {
95+
if (gameId === game) continue;
96+
const match = gameDeclarations.find((d) => d.name === name && d.kind === kind);
97+
if (match) {
98+
const gameInfo = GAMES.find((g) => g.id === gameId);
99+
otherGames.push({
100+
gameId,
101+
gameName: gameInfo?.name ?? gameId,
102+
status: areDeclarationsEqual(current, match) ? "identical" : "differs",
103+
module: match.module,
104+
});
105+
}
106+
}
107+
108+
if (otherGames.length === 0) return null;
109+
110+
return (
111+
<Wrapper>
112+
<Title>Also in</Title>
113+
<RefList>
114+
{otherGames.map(({ gameId, gameName, status, module: otherModule }) => (
115+
<GameLink key={gameId} to={`/${gameId}/${otherModule}/${name}`} $status={status} title={status === "identical" ? "Identical" : "Differs"}>
116+
<StatusDot $status={status} />
117+
{gameName}
118+
</GameLink>
119+
))}
120+
</RefList>
121+
</Wrapper>
122+
);
123+
}

web/src/components/Docs/DeclarationsContext.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createContext } from "react";
22
import { Declaration } from "~components/Docs/api";
3+
import { GameId } from "~games";
34

45
export type ReferenceEntry = {
56
declarationName: string;
@@ -10,9 +11,17 @@ export type ReferenceEntry = {
1011
};
1112

1213
export type DeclarationsContextType = {
14+
game: GameId;
1315
root: string;
1416
declarations: Declaration[];
1517
references: Map<string, ReferenceEntry[]>;
18+
allGames: Map<GameId, Declaration[]> | null;
1619
};
1720

18-
export const DeclarationsContext = createContext<DeclarationsContextType>({ root: "", declarations: [], references: new Map() });
21+
export const DeclarationsContext = createContext<DeclarationsContextType>({
22+
game: "cs2",
23+
root: "",
24+
declarations: [],
25+
references: new Map(),
26+
allGames: null,
27+
});

web/src/components/Docs/SchemaClass.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import styled from "styled-components";
44
import * as api from "./api";
55
import { SchemaTypeView, MetadataTags } from "./SchemaType";
66
import { ReferencedBy } from "./ReferencedBy";
7+
import { CrossGameRefs } from "./CrossGameRefs";
78
import { KindIcon } from "./utils/components";
89
import { DeclarationsContext } from "./DeclarationsContext";
910
import { useSearchWords, useSearchOffsets } from "./utils/filtering";
@@ -148,6 +149,7 @@ export const SchemaClassView: React.FC<{
148149
</ClassMembers>
149150
)}
150151
{!collapseNonMatching && <ReferencedBy name={declaration.name} module={declaration.module} />}
152+
{!collapseNonMatching && <CrossGameRefs name={declaration.name} module={declaration.module} kind="class" />}
151153
</CommonGroupWrapper>
152154
);
153155
};

web/src/components/Docs/SchemaEnum.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { KindIcon } from "./utils/components";
66
import { DeclarationsContext } from "./DeclarationsContext";
77
import { MetadataTags } from "./SchemaType";
88
import { ReferencedBy } from "./ReferencedBy";
9+
import { CrossGameRefs } from "./CrossGameRefs";
910
import { ModuleBadge, matchesWords } from "./SchemaClass";
1011
import { useSearchWords } from "./utils/filtering";
1112
import {
@@ -92,6 +93,7 @@ export const SchemaEnumView: React.FC<{
9293
</EnumMembers>
9394
)}
9495
{!collapseNonMatching && <ReferencedBy name={declaration.name} module={declaration.module} />}
96+
{!collapseNonMatching && <CrossGameRefs name={declaration.name} module={declaration.module} kind="enum" />}
9597
</CommonGroupWrapper>
9698
);
9799
};

web/src/components/Search/index.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,23 @@ export const SearchInput = styled.input.attrs({ type: "search" })`
2525
width: 100%;
2626
box-sizing: border-box;
2727
padding: 8px 14px;
28-
border: 1px solid ${(props) => props.theme.groupBorder};
29-
border-radius: 10px;
28+
border: none;
29+
border-radius: 8px;
3030
background: ${(props) => props.theme.searchbox.background};
3131
color: ${(props) => props.theme.text};
3232
font-family: inherit;
3333
font-size: 16px;
3434
outline: none;
35-
transition: border-color 0.15s, box-shadow 0.15s;
35+
transition: box-shadow 0.15s;
3636
37-
&:focus {
38-
border-color: ${(props) => props.theme.highlight};
37+
&:hover {
3938
box-shadow: 0 0 0 2px ${(props) => props.theme.highlight}30;
4039
}
4140
41+
&:focus {
42+
box-shadow: 0 0 0 2px ${(props) => props.theme.highlight};
43+
}
44+
4245
&::placeholder {
4346
color: ${(props) => props.theme.searchbox.placeholder};
4447
}

web/src/components/Themes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const themeDark = {
6161
},
6262

6363
searchbox: {
64-
background: "#181a21",
64+
background: "#090e15",
6565
placeholder: "#6b7280",
6666
border: "none",
6767
button: "transparent",

0 commit comments

Comments
 (0)