aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/App.tsx6
-rw-r--r--frontend/src/api/Api.ts2
-rw-r--r--frontend/src/api/Maps.ts10
-rw-r--r--frontend/src/components/UploadRunDialog.tsx113
-rw-r--r--frontend/src/types/Content.ts1
-rw-r--r--frontend/src/types/MapNames.ts127
6 files changed, 185 insertions, 74 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index e4bde75..a02779b 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -66,12 +66,6 @@ const App: React.FC = () => {
66 _fetch_games(); 66 _fetch_games();
67 }, []); 67 }, []);
68 68
69 if (!games) {
70 return (
71 <></>
72 )
73 };
74
75 return ( 69 return (
76 <> 70 <>
77 <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => { 71 <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => {
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts
index 2e55ab4..4a3f907 100644
--- a/frontend/src/api/Api.ts
+++ b/frontend/src/api/Api.ts
@@ -35,7 +35,7 @@ export const API = {
35 35
36 post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), 36 post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content),
37 post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), 37 post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment),
38 post_record: (token: string, run: UploadRunContent) => post_record(token, run), 38 post_record: (token: string, run: UploadRunContent, map_id: number) => post_record(token, run, map_id),
39 39
40 delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), 40 delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id),
41 41
diff --git a/frontend/src/api/Maps.ts b/frontend/src/api/Maps.ts
index 89657b5..3832a2e 100644
--- a/frontend/src/api/Maps.ts
+++ b/frontend/src/api/Maps.ts
@@ -73,9 +73,9 @@ export const delete_map_discussion = async (token: string, map_id: string, discu
73 return response.data.success; 73 return response.data.success;
74}; 74};
75 75
76export const post_record = async (token: string, run: UploadRunContent): Promise<[boolean, string]> => { 76export const post_record = async (token: string, run: UploadRunContent, map_id: number): Promise<[boolean, string]> => {
77 if (run.partner_demo) { 77 if (run.partner_demo) {
78 const response = await axios.postForm(url(`maps/${run.map_id}/record`), { 78 const response = await axios.postForm(url(`maps/${map_id}/record`), {
79 "host_demo": run.host_demo, 79 "host_demo": run.host_demo,
80 "partner_demo": run.partner_demo, 80 "partner_demo": run.partner_demo,
81 }, { 81 }, {
@@ -83,16 +83,16 @@ export const post_record = async (token: string, run: UploadRunContent): Promise
83 "Authorization": token, 83 "Authorization": token,
84 } 84 }
85 }); 85 });
86 return [ response.data.success, response.data.message ]; 86 return [response.data.success, response.data.message];
87 } else { 87 } else {
88 const response = await axios.postForm(url(`maps/${run.map_id}/record`), { 88 const response = await axios.postForm(url(`maps/${map_id}/record`), {
89 "host_demo": run.host_demo, 89 "host_demo": run.host_demo,
90 }, { 90 }, {
91 headers: { 91 headers: {
92 "Authorization": token, 92 "Authorization": token,
93 } 93 }
94 }); 94 });
95 return [ response.data.success, response.data.message ]; 95 return [response.data.success, response.data.message];
96 } 96 }
97} 97}
98 98
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index 118b589..c02fdb8 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -4,12 +4,12 @@ import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp';
4 4
5import '@css/UploadRunDialog.css'; 5import '@css/UploadRunDialog.css';
6import { Game } from '@customTypes/Game'; 6import { Game } from '@customTypes/Game';
7import { Map } from '@customTypes/Map';
8import { API } from '@api/Api'; 7import { API } from '@api/Api';
9import { useNavigate } from 'react-router-dom'; 8import { useNavigate } from 'react-router-dom';
10import useMessage from '@hooks/UseMessage'; 9import useMessage from '@hooks/UseMessage';
11import useConfirm from '@hooks/UseConfirm'; 10import useConfirm from '@hooks/UseConfirm';
12import useMessageLoad from "@hooks/UseMessageLoad"; 11import useMessageLoad from "@hooks/UseMessageLoad";
12import { MapNames } from '@customTypes/MapNames';
13 13
14interface UploadRunDialogProps { 14interface UploadRunDialogProps {
15 token?: string; 15 token?: string;
@@ -27,19 +27,11 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
27 const navigate = useNavigate(); 27 const navigate = useNavigate();
28 28
29 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ 29 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({
30 map_id: 0,
31 host_demo: null, 30 host_demo: null,
32 partner_demo: null, 31 partner_demo: null,
33 }); 32 });
34 33
35 const [currentMap, setCurrentMap] = React.useState<string>("");
36
37 const _set_current_map = (game_name: string) => {
38 setCurrentMap(game_name);
39 }
40
41 const [selectedGameID, setSelectedGameID] = React.useState<number>(0); 34 const [selectedGameID, setSelectedGameID] = React.useState<number>(0);
42 const [selectedGameMaps, setSelectedGameMaps] = React.useState<Map[]>([]);
43 const [selectedGameName, setSelectedGameName] = React.useState<string>(""); 35 const [selectedGameName, setSelectedGameName] = React.useState<string>("");
44 36
45 // dropdowns 37 // dropdowns
@@ -50,6 +42,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
50 42
51 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); 43 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false);
52 const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); 44 const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false);
45
53 const fileInputRef = React.useRef<HTMLInputElement>(null); 46 const fileInputRef = React.useRef<HTMLInputElement>(null);
54 const fileInputRefPartner = React.useRef<HTMLInputElement>(null); 47 const fileInputRefPartner = React.useRef<HTMLInputElement>(null);
55 48
@@ -102,14 +95,6 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
102 95
103 const _handle_game_select = async (game_id: string, game_name: string) => { 96 const _handle_game_select = async (game_id: string, game_name: string) => {
104 setLoading(true); 97 setLoading(true);
105 const gameMaps = await API.get_game_maps(game_id);
106 setSelectedGameMaps(gameMaps);
107 setUploadRunContent({
108 map_id: gameMaps.find((map) => !map.is_disabled)!.id, //gameMaps[0].id,
109 host_demo: null,
110 partner_demo: null,
111 });
112 _set_current_map(gameMaps.find((map) => !map.is_disabled)!.name);
113 setSelectedGameID(parseInt(game_id) - 1); 98 setSelectedGameID(parseInt(game_id) - 1);
114 setSelectedGameName(game_name); 99 setSelectedGameName(game_name);
115 setLoading(false); 100 setLoading(false);
@@ -158,6 +143,20 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
158 await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") 143 await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.")
159 return 144 return
160 } 145 }
146
147 if (!demo.mapName || !MapNames[demo.mapName]) {
148 await message("Error", "Error while processing demo: Invalid map name.")
149 return
150 }
151
152 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) {
153 await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.")
154 return
155 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) {
156 await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.")
157 return
158 }
159
161 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; 160 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {};
162 161
163 const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`); 162 const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`);
@@ -167,10 +166,14 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
167 } 166 }
168 167
169 messageLoad("Uploading..."); 168 messageLoad("Uploading...");
170 const [success, response] = await API.post_record(token, uploadRunContent); 169 const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]);
171 messageLoadClose(); 170 messageLoadClose();
172 await message("Upload Record", response); 171 await message("Upload Record", response);
173 if (success) { 172 if (success) {
173 setUploadRunContent({
174 host_demo: null,
175 partner_demo: null,
176 });
174 onClose(success); 177 onClose(success);
175 navigate("/profile"); 178 navigate("/profile");
176 } 179 }
@@ -179,7 +182,6 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
179 182
180 React.useEffect(() => { 183 React.useEffect(() => {
181 if (open) { 184 if (open) {
182
183 setDragHighlightPartner(false); 185 setDragHighlightPartner(false);
184 setDragHighlight(false); 186 setDragHighlight(false);
185 _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. 187 _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?.
@@ -203,37 +205,20 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
203 <div className='dropdown-cur'>{selectedGameName}</div> 205 <div className='dropdown-cur'>{selectedGameName}</div>
204 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> 206 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i>
205 </div> 207 </div>
206 <div style={{top: "110px"}} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> 208 <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}>
207 {games.map((game) => ( 209 {games.map((game) => (
208 <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> 210 <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div>
209 ))} 211 ))}
210 </div> 212 </div>
211 {!loading && ( 213 </div>
212 <>
213 <div style={{ padding: "25px 0px" }}>
214 <h3 style={{ margin: "0px 0px" }}>Select Map</h3>
215 <div onClick={() => _handle_dropdowns(2)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}>
216 <span style={{ userSelect: "none" }}>{currentMap}</span>
217 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i>
218 </div>
219 </div>
220 <div style={{top: "220px"}} id='dropdown2' className={dropdown2Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}>
221 {selectedGameMaps && selectedGameMaps.filter(gameMap => !gameMap.is_disabled).map((gameMap) => (
222 <div onClick={() => { setUploadRunContent({ ...uploadRunContent, map_id: gameMap.id }); _set_current_map(gameMap.name); _handle_dropdowns(2); }} key={gameMap.id}>{gameMap.name}</div>
223 ))}
224 </div>
225 </>
226
227 )}
228 </div>
229 214
230 { 215 {
231 !loading && 216 !loading &&
232 ( 217 (
233 <> 218 <>
234 219
235 <div> 220 <div>
236 <h3 style={{margin: "10px 0px"}}>Host Demo</h3> 221 <h3 style={{ margin: "10px 0px" }}>Host Demo</h3>
237 <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}> 222 <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}>
238 <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> 223 <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} />
239 {!uploadRunContent.host_demo ? 224 {!uploadRunContent.host_demo ?
@@ -252,38 +237,44 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
252 games[selectedGameID].is_coop && 237 games[selectedGameID].is_coop &&
253 ( 238 (
254 <> 239 <>
255 <div> 240 <div>
256 <h3 style={{margin: "10px 0px"}}>Partner Demo</h3> 241 <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3>
257 <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}> 242 <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}>
258 <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ? 243 <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ?
259 <div>
260 <span>Drag and drop</span>
261 <div> 244 <div>
262 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> 245 <span>Drag and drop</span>
263 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> 246 <div>
247 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br />
248 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button>
249 </div>
264 </div> 250 </div>
265 </div> 251 : null}
266 : null}
267 252
268 <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span> 253 <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span>
254 </div>
269 </div> 255 </div>
270 </div>
271 </> 256 </>
272 ) 257 )
273 } 258 }
274 </div> 259 </div>
275 <div className='search-container'> 260 <div className='search-container'>
261
262 </div>
276 263
277 </div>
278
279 </> 264 </>
280 ) 265 )
281 } 266 }
282 </div> 267 </div>
283 <div className='upload-run-buttons-container'> 268 <div className='upload-run-buttons-container'>
284 <button onClick={_upload_run}>Submit</button> 269 <button onClick={_upload_run}>Submit</button>
285 <button onClick={() => onClose(false)}>Cancel</button> 270 <button onClick={() => {
286 </div> 271 onClose(false);
272 setUploadRunContent({
273 host_demo: null,
274 partner_demo: null,
275 });
276 }}>Cancel</button>
277 </div>
287 </div> 278 </div>
288 </div> 279 </div>
289 </> 280 </>
diff --git a/frontend/src/types/Content.ts b/frontend/src/types/Content.ts
index 42a6917..775fab4 100644
--- a/frontend/src/types/Content.ts
+++ b/frontend/src/types/Content.ts
@@ -18,7 +18,6 @@ export interface MapDiscussionCommentContent {
18}; 18};
19 19
20export interface UploadRunContent { 20export interface UploadRunContent {
21 map_id: number;
22 host_demo: File | null; 21 host_demo: File | null;
23 partner_demo: File | null; 22 partner_demo: File | null;
24}; 23};
diff --git a/frontend/src/types/MapNames.ts b/frontend/src/types/MapNames.ts
new file mode 100644
index 0000000..b6313e7
--- /dev/null
+++ b/frontend/src/types/MapNames.ts
@@ -0,0 +1,127 @@
1export const MapNames: { [key: string]: number } = {
2 "sp_a1_intro1": 1,
3 "sp_a1_intro2": 2,
4 "sp_a1_intro3": 3,
5 "sp_a1_intro4": 4,
6 "sp_a1_intro5": 5,
7 "sp_a1_intro6": 6,
8 "sp_a1_intro7": 7,
9 "sp_a1_wakeup": 8,
10 "sp_a2_intro": 9,
11
12 "sp_a2_laser_intro": 10,
13 "sp_a2_laser_stairs": 11,
14 "sp_a2_dual_lasers": 12,
15 "sp_a2_laser_over_goo": 13,
16 "sp_a2_catapult_intro": 14,
17 "sp_a2_trust_fling": 15,
18 "sp_a2_pit_flings": 16,
19 "sp_a2_fizzler_intro": 17,
20
21 "sp_a2_sphere_peek": 18,
22 "sp_a2_ricochet": 19,
23 "sp_a2_bridge_intro": 20,
24 "sp_a2_bridge_the_gap": 21,
25 "sp_a2_turret_intro": 22,
26 "sp_a2_laser_relays": 23,
27 "sp_a2_turret_blocker": 24,
28 "sp_a2_laser_vs_turret": 25,
29 "sp_a2_pull_the_rug": 26,
30
31 "sp_a2_column_blocker": 27,
32 "sp_a2_laser_chaining": 28,
33 "sp_a2_triple_laser": 29,
34 "sp_a2_bts1": 30,
35 "sp_a2_bts2": 31,
36
37 "sp_a2_bts3": 32,
38 "sp_a2_bts4": 33,
39 "sp_a2_bts5": 34,
40 "sp_a2_core": 35,
41
42 "sp_a3_01": 36,
43 "sp_a3_03": 37,
44 "sp_a3_jump_intro": 38,
45 "sp_a3_bomb_flings": 39,
46 "sp_a3_crazy_box": 40,
47 "sp_a3_transition01": 41,
48
49 "sp_a3_speed_ramp": 42,
50 "sp_a3_speed_flings": 43,
51 "sp_a3_portal_intro": 44,
52 "sp_a3_end": 45,
53
54 "sp_a4_intro": 46,
55 "sp_a4_tb_intro": 47,
56 "sp_a4_tb_trust_drop": 48,
57 "sp_a4_tb_wall_button": 49,
58 "sp_a4_tb_polarity": 50,
59 "sp_a4_tb_catch": 51,
60 "sp_a4_stop_the_box": 52,
61 "sp_a4_laser_catapult": 53,
62 "sp_a4_laser_platform": 54,
63 "sp_a4_speed_tb_catch": 55,
64 "sp_a4_jump_polarity": 56,
65
66 "sp_a4_finale1": 57,
67 "sp_a4_finale2": 58,
68 "sp_a4_finale3": 59,
69 "sp_a4_finale4": 60,
70
71 "mp_coop_start": 61,
72 "mp_coop_lobby_3": 62,
73
74 "mp_coop_doors": 63,
75 "mp_coop_race_2": 64,
76 "mp_coop_laser_2": 65,
77 "mp_coop_rat_maze": 66,
78 "mp_coop_laser_crusher": 67,
79 "mp_coop_teambts": 68,
80
81 "mp_coop_fling_3": 69,
82 "mp_coop_infinifling_train": 70,
83 "mp_coop_come_along": 71,
84 "mp_coop_fling_1": 72,
85 "mp_coop_catapult_1": 73,
86 "mp_coop_multifling_1": 74,
87 "mp_coop_fling_crushers": 75,
88 "mp_coop_fan": 76,
89
90 "mp_coop_wall_intro": 77,
91 "mp_coop_wall_2": 78,
92 "mp_coop_catapult_wall_intro": 79,
93 "mp_coop_wall_block": 80,
94 "mp_coop_catapult_2": 81,
95 "mp_coop_turret_walls": 82,
96 "mp_coop_turret_ball": 83,
97 "mp_coop_wall_5": 84,
98
99 "mp_coop_tbeam_redirect": 85,
100 "mp_coop_tbeam_drill": 86,
101 "mp_coop_tbeam_catch_grind_1": 87,
102 "mp_coop_tbeam_laser_1": 88,
103 "mp_coop_tbeam_polarity": 89,
104 "mp_coop_tbeam_polarity2": 90,
105 "mp_coop_tbeam_polarity3": 91,
106 "mp_coop_tbeam_maze": 92,
107 "mp_coop_tbeam_end": 93,
108
109 "mp_coop_paint_come_along": 94,
110 "mp_coop_paint_redirect": 95,
111 "mp_coop_paint_bridge": 96,
112 "mp_coop_paint_walljumps": 97,
113 "mp_coop_paint_speed_fling": 98,
114 "mp_coop_paint_red_racer": 99,
115 "mp_coop_paint_speed_catch": 100,
116 "mp_coop_paint_longjump_intro": 101,
117
118 "mp_coop_separation_1": 102,
119 "mp_coop_tripleaxis": 103,
120 "mp_coop_catapult_catch": 104,
121 "mp_coop_2paints_1bridge": 105,
122 "mp_coop_paint_conversion": 106,
123 "mp_coop_bridge_catch": 107,
124 "mp_coop_laser_tbeam": 108,
125 "mp_coop_paint_rat_maze": 109,
126 "mp_coop_paint_crazy_box": 110,
127};