alexrsworld 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Apps/30dollarwebsite.html +403 -0
- package/Apps/emulatorjs.html +258 -0
- package/Apps/ruffle.html +93 -0
- package/Apps/soundboard.html +4 -0
- package/Games/WAflash/connect4.html +87 -0
- package/Games/WAflash/dealornodeal.html +89 -0
- package/Games/WAflash/escapingtheprison.html +87 -0
- package/Games/WAflash/fancypants2.html +86 -0
- package/Games/WAflash/freegear.html +87 -0
- package/Games/WAflash/learntoflyidle.html +87 -0
- package/Games/WAflash/papascoop.html +88 -0
- package/Games/WAflash/papashotdog.html +88 -0
- package/Games/WAflash/papaspancake.html +89 -0
- package/Games/WAflash/papaswingeria.html +87 -0
- package/Games/WAflash/pottyracers3.html +86 -0
- package/Games/WAflash/run.html +88 -0
- package/Games/WAflash/run2.html +88 -0
- package/Games/WAflash/shoppingcarthero2.html +88 -0
- package/Games/WAflash/sonicultimate.html +87 -0
- package/Games/WAflash/sugarsugar.html +91 -0
- package/Games/WAflash/vex.html +91 -0
- package/Games/WAflash/worldshardestgame3.html +89 -0
- package/Games/emulated/GBA/emeraldversion.html +30 -0
- package/Games/emulated/GBA/fireredversion.html +30 -0
- package/Games/emulated/GBA/leafgreenversion.html +30 -0
- package/Games/emulated/GBA/rubyversion.html +30 -0
- package/Games/emulated/GBA/sapphireversion.html +30 -0
- package/Games/emulated/N64/mariokart64.html +27 -0
- package/Games/emulated/N64/supermario64.html +30 -0
- package/Games/emulated/SNES/earthbound.html +31 -0
- package/Games/ruffle/MahjongTower.html +40 -0
- package/Games/ruffle/QWOP.html +41 -0
- package/Games/ruffle/achievementunlocked.html +41 -0
- package/Games/ruffle/achievementunlocked2.html +41 -0
- package/Games/ruffle/achievementunlocked3.html +41 -0
- package/Games/ruffle/ageofdefense.html +41 -0
- package/Games/ruffle/ageofwar.html +41 -0
- package/Games/ruffle/ageofwar2.html +41 -0
- package/Games/ruffle/airship.html +41 -0
- package/Games/ruffle/alien.html +41 -0
- package/Games/ruffle/angrybirdshalloween.html +40 -0
- package/Games/ruffle/appleshooter.html +41 -0
- package/Games/ruffle/axisfootballleague.html +40 -0
- package/Games/ruffle/badpiggies.html +41 -0
- package/Games/ruffle/bejeweledtwist.html +41 -0
- package/Games/ruffle/blinding.html +40 -0
- package/Games/ruffle/bloons.html +41 -0
- package/Games/ruffle/bloonsupermonkey.html +40 -0
- package/Games/ruffle/bloxorz.html +40 -0
- package/Games/ruffle/bobtherobber.html +41 -0
- package/Games/ruffle/bowling.html +40 -0
- package/Games/ruffle/breakingthebank.html +40 -0
- package/Games/ruffle/btd.html +40 -0
- package/Games/ruffle/btd2.html +40 -0
- package/Games/ruffle/btd3.html +41 -0
- package/Games/ruffle/btd4.html +41 -0
- package/Games/ruffle/btd4expansion.html +41 -0
- package/Games/ruffle/btd5.html +41 -0
- package/Games/ruffle/bubbleshooter.html +41 -0
- package/Games/ruffle/burgerrun.html +41 -0
- package/Games/ruffle/burritobisonrevenge.html +41 -0
- package/Games/ruffle/crimsonroom.html +41 -0
- package/Games/ruffle/cubefield.html +40 -0
- package/Games/ruffle/curveball.html +41 -0
- package/Games/ruffle/deadzed2.html +40 -0
- package/Games/ruffle/dinerdash.html +41 -0
- package/Games/ruffle/donkeykong.html +40 -0
- package/Games/ruffle/douchebagworkout.html +40 -0
- package/Games/ruffle/douchebagworkout2.html +40 -0
- package/Games/ruffle/ducklife.html +40 -0
- package/Games/ruffle/ducklife2.html +40 -0
- package/Games/ruffle/ducklife3.html +40 -0
- package/Games/ruffle/ducklife4.html +40 -0
- package/Games/ruffle/electricman.html +41 -0
- package/Games/ruffle/electricman2.html +41 -0
- package/Games/ruffle/factoryballs.html +42 -0
- package/Games/ruffle/factoryballs2.html +42 -0
- package/Games/ruffle/factoryballs3.html +41 -0
- package/Games/ruffle/factoryballs4.html +41 -0
- package/Games/ruffle/fancypants.html +41 -0
- package/Games/ruffle/fancypants3.html +41 -0
- package/Games/ruffle/fleeingthecomplex.html +41 -0
- package/Games/ruffle/fltron.html +40 -0
- package/Games/ruffle/foresttemple.html +40 -0
- package/Games/ruffle/frogger.html +40 -0
- package/Games/ruffle/frontlinedefense.html +40 -0
- package/Games/ruffle/galagaflash.html +41 -0
- package/Games/ruffle/gravitee.html +41 -0
- package/Games/ruffle/gravitee2.html +41 -0
- package/Games/ruffle/growcube.html +41 -0
- package/Games/ruffle/growisland.html +41 -0
- package/Games/ruffle/growvalley.html +41 -0
- package/Games/ruffle/impossiblequiz.html +41 -0
- package/Games/ruffle/impossiblequiz2.html +41 -0
- package/Games/ruffle/injustice.html +41 -0
- package/Games/ruffle/isoball.html +41 -0
- package/Games/ruffle/johnnyupgrade.html +41 -0
- package/Games/ruffle/jumpingfinn.html +40 -0
- package/Games/ruffle/kaboomz.html +40 -0
- package/Games/ruffle/leaguebowling.html +41 -0
- package/Games/ruffle/learntofly.html +40 -0
- package/Games/ruffle/learntofly2.html +40 -0
- package/Games/ruffle/learntofly3.html +40 -0
- package/Games/ruffle/lemonade.html +40 -0
- package/Games/ruffle/madness.html +40 -0
- package/Games/ruffle/madnessa.html +40 -0
- package/Games/ruffle/mahjonggardens.html +40 -0
- package/Games/ruffle/mahjongtitans.html +40 -0
- package/Games/ruffle/mariocombat.html +40 -0
- package/Games/ruffle/minecrafttowerdefense.html +41 -0
- package/Games/ruffle/minecrafttowerdefense2.html +41 -0
- package/Games/ruffle/motherload.html +40 -0
- package/Games/ruffle/murder.html +41 -0
- package/Games/ruffle/myfriendpedro.html +40 -0
- package/Games/ruffle/neonrider.html +41 -0
- package/Games/ruffle/nyancatlostinspace.html +40 -0
- package/Games/ruffle/pacman.html +40 -0
- package/Games/ruffle/pacxon.html +41 -0
- package/Games/ruffle/pacxondeluxe.html +41 -0
- package/Games/ruffle/pandemic2.html +41 -0
- package/Games/ruffle/papas sushi.html +40 -0
- package/Games/ruffle/papasbake.html +40 -0
- package/Games/ruffle/papasburgeria.html +41 -0
- package/Games/ruffle/papascheese.html +40 -0
- package/Games/ruffle/papascupcake.html +40 -0
- package/Games/ruffle/papasdonut.html +40 -0
- package/Games/ruffle/papaspasta.html +42 -0
- package/Games/ruffle/papaspizza.html +41 -0
- package/Games/ruffle/papastaco.html +42 -0
- package/Games/ruffle/plantsvszombies.html +41 -0
- package/Games/ruffle/poppit.html +42 -0
- package/Games/ruffle/portaltheflashversion.html +42 -0
- package/Games/ruffle/pottyracers.html +40 -0
- package/Games/ruffle/pottyracers2.html +40 -0
- package/Games/ruffle/raftwars.html +40 -0
- package/Games/ruffle/redball.html +42 -0
- package/Games/ruffle/redball2.html +41 -0
- package/Games/ruffle/redball3.html +41 -0
- package/Games/ruffle/riddleschool.html +41 -0
- package/Games/ruffle/riddleschool2.html +41 -0
- package/Games/ruffle/riddleschool3.html +41 -0
- package/Games/ruffle/riddleschool4.html +41 -0
- package/Games/ruffle/riddleschool5.html +41 -0
- package/Games/ruffle/riddletransfer.html +41 -0
- package/Games/ruffle/riddletransfer2.html +41 -0
- package/Games/ruffle/rollercoaster.html +41 -0
- package/Games/ruffle/shoppingcarthero.html +41 -0
- package/Games/ruffle/spaceiskey.html +41 -0
- package/Games/ruffle/spaceiskey2.html +41 -0
- package/Games/ruffle/sprinter.html +41 -0
- package/Games/ruffle/stealingthediamond.html +41 -0
- package/Games/ruffle/stickwar.html +41 -0
- package/Games/ruffle/strikeforceheros.html +40 -0
- package/Games/ruffle/strikeforceheros2.html +40 -0
- package/Games/ruffle/strikeforcekitty.html +41 -0
- package/Games/ruffle/sugarsugar2.html +41 -0
- package/Games/ruffle/superfighters.html +41 -0
- package/Games/ruffle/supermarioflash.html +41 -0
- package/Games/ruffle/supermarioflash2.html +41 -0
- package/Games/ruffle/supermarioflash3.html +41 -0
- package/Games/ruffle/supersmashflash.html +41 -0
- package/Games/ruffle/tactical.html +41 -0
- package/Games/ruffle/tactical2.html +41 -0
- package/Games/ruffle/tacticaloriginal.html +41 -0
- package/Games/ruffle/territorywar.html +41 -0
- package/Games/ruffle/tetrix2.html +41 -0
- package/Games/ruffle/thefightforglorton.html +41 -0
- package/Games/ruffle/thinice.html +41 -0
- package/Games/ruffle/treasurehunt.html +40 -0
- package/Games/ruffle/unfairmario.html +40 -0
- package/Games/ruffle/vex2.html +41 -0
- package/Games/ruffle/whenpizzaattacks.html +41 -0
- package/Games/ruffle/whg.html +40 -0
- package/Games/ruffle/whg2.html +40 -0
- package/Games/ruffle/whg4.html +40 -0
- package/Games/ruffle/yahootennis.html +40 -0
- package/Games/ruffle/zombocalypse.html +41 -0
- package/Games/singlefile.html +67 -0
- package/Games/sonic-games/sonicthehedgehog.html +29 -0
- package/Games/sonic-games/sonicthehedgehog2.html +31 -0
- package/Games/sonic-games/sonicthehedgehog3.html +23 -0
- package/Games/standalone/10minutestilldawn.html +85 -0
- package/Games/standalone/1v1lol.html +299 -0
- package/Games/standalone/2048.html +129 -0
- package/Games/standalone/2048cupcakes.html +135 -0
- package/Games/standalone/Angry Birds.html +20 -0
- package/Games/standalone/BlockPost.html +36 -0
- package/Games/standalone/BuildNow.gg.html +136 -0
- package/Games/standalone/Flappy Dunk.html +169 -0
- package/Games/standalone/Granny 2.html +202 -0
- package/Games/standalone/Time Shooter 2.html +94 -0
- package/Games/standalone/adventure.html +128 -0
- package/Games/standalone/angrybirdsshowdown.html +24 -0
- package/Games/standalone/badicecream.html +163 -0
- package/Games/standalone/badicecream2.html +165 -0
- package/Games/standalone/badicecream3.html +165 -0
- package/Games/standalone/badtime.html +145 -0
- package/Games/standalone/baldi.html +26 -0
- package/Games/standalone/basketrandom.html +46 -0
- package/Games/standalone/bigtower.html +65 -0
- package/Games/standalone/bigtower2.html +20 -0
- package/Games/standalone/bitlife.html +22 -0
- package/Games/standalone/blockblast.html +94 -0
- package/Games/standalone/blockthepig.html +142 -0
- package/Games/standalone/bowmasters.html +27 -0
- package/Games/standalone/boxingrandom.html +0 -0
- package/Games/standalone/candycrush.html +67 -0
- package/Games/standalone/carssimulator.html +40 -0
- package/Games/standalone/caseclicker.html +310 -0
- package/Games/standalone/cellmachine.html +24 -0
- package/Games/standalone/choppyorc.html +210 -0
- package/Games/standalone/circleo.html +99 -0
- package/Games/standalone/clusterrush.html +47 -0
- package/Games/standalone/colorswitch.html +224 -0
- package/Games/standalone/colortunnel.html +24 -0
- package/Games/standalone/cookieclicker.html +150 -0
- package/Games/standalone/crazycattle3d.html +246 -0
- package/Games/standalone/crossyroad.html +23 -0
- package/Games/standalone/cyberpunkracing.html +37 -0
- package/Games/standalone/dadish.html +46 -0
- package/Games/standalone/dadish2.html +72 -0
- package/Games/standalone/deathrun3d.html +69 -0
- package/Games/standalone/dogeminer.html +996 -0
- package/Games/standalone/dragonvsbricks.html +48 -0
- package/Games/standalone/driftboss.html +146 -0
- package/Games/standalone/drifthunters.html +123 -0
- package/Games/standalone/drivemad.html +41 -0
- package/Games/standalone/ducklifebattle.html +131 -0
- package/Games/standalone/ducklifespace.html +93 -0
- package/Games/standalone/earntodie.html +117 -0
- package/Games/standalone/economical.html +77 -0
- package/Games/standalone/fireandice.html +22 -0
- package/Games/standalone/flappybird.html +137 -0
- package/Games/standalone/fnaf.html +115 -0
- package/Games/standalone/fnaf2.html +34 -0
- package/Games/standalone/fnaf3.html +337 -0
- package/Games/standalone/fnaf4.html +338 -0
- package/Games/standalone/fnaf4halloween.html +34 -0
- package/Games/standalone/fridaynightfunkin.html +76 -0
- package/Games/standalone/fruitninja.html +99 -0
- package/Games/standalone/galaga.html +216 -0
- package/Games/standalone/gdlite.html +255 -0
- package/Games/standalone/gladihoppers.html +36 -0
- package/Games/standalone/googlefeud.html +1163 -0
- package/Games/standalone/granny.html +233 -0
- package/Games/standalone/grindcraft.html +44 -0
- package/Games/standalone/happywheels.html +19 -0
- package/Games/standalone/helixjump.html +30 -0
- package/Games/standalone/holeio.html +53 -0
- package/Games/standalone/idlebreakout.html +95 -0
- package/Games/standalone/jetpack.html +21 -0
- package/Games/standalone/magictiles3.html +111 -0
- package/Games/standalone/monkeymart.html +365 -0
- package/Games/standalone/motox3m.html +1 -0
- package/Games/standalone/motox3mpoolparty.html +18 -0
- package/Games/standalone/motox3mspookyland.html +68 -0
- package/Games/standalone/ngon.html +577 -0
- package/Games/standalone/omnombounce.html +82 -0
- package/Games/standalone/pacmanoriginal.html +30 -0
- package/Games/standalone/papasfreezeria.html +348 -0
- package/Games/standalone/paperio2.html +54 -0
- package/Games/standalone/parkingfury.html +45 -0
- package/Games/standalone/polytrack.html +40 -0
- package/Games/standalone/pou.html +73 -0
- package/Games/standalone/retrobowl.html +119 -0
- package/Games/standalone/retrobowlcollege.html +179 -0
- package/Games/standalone/rocketsoccer.html +39 -0
- package/Games/standalone/run3.html +64 -0
- package/Games/standalone/slender.html +40 -0
- package/Games/standalone/slope.html +59 -0
- package/Games/standalone/snowrider3d.html +27 -0
- package/Games/standalone/soccerrandom.html +0 -0
- package/Games/standalone/solitaire.html +32 -0
- package/Games/standalone/sonicroboblast2.html +2169 -0
- package/Games/standalone/spacewaves.html +239 -0
- package/Games/standalone/stack.html +639 -0
- package/Games/standalone/station141.html +27 -0
- package/Games/standalone/stickmangolf.html +22 -0
- package/Games/standalone/stickmanhook.html +39 -0
- package/Games/standalone/stuntcars3.html +26 -0
- package/Games/standalone/subwaysurfers.html +55 -0
- package/Games/standalone/superhot.html +35 -0
- package/Games/standalone/superliquidsoccer.html +311 -0
- package/Games/standalone/templerun2.html +38 -0
- package/Games/standalone/timeshooter.html +99 -0
- package/Games/standalone/tinyfishing.html +139 -0
- package/Games/standalone/tombofthemask.html +14 -0
- package/Games/standalone/triviacrack.html +24 -0
- package/Games/standalone/tubejumpers.html +20 -0
- package/Games/standalone/ultimatecustomnight.html +34 -0
- package/Games/standalone/undertheredsky.html +46 -0
- package/Games/standalone/vex3.html +42 -0
- package/Games/standalone/vex4.html +38 -0
- package/Games/standalone/vex5.html +43 -0
- package/Games/standalone/vex6.html +56 -0
- package/Games/standalone/vex7.html +53 -0
- package/Games/standalone/vex8.html +46 -0
- package/Games/standalone/wordle.html +27 -0
- package/Games/standalone/worldtour.html +95 -0
- package/backup.html +2016 -0
- package/games.json +2373 -0
- package/index.html +2302 -0
- package/new logo.png +0 -0
- package/package.json +21 -0
- package/port.html +0 -0
- package/readme.md +1 -0
- package/singlefilegames.json +2266 -0
- package/sounds/ambience.mp3 +0 -0
- package/sounds/click.mp3 +0 -0
- package/sounds/close.mp3 +0 -0
- package/sounds/hover.mp3 +0 -0
- package/sounds/select.mp3 +0 -0
|
@@ -0,0 +1,2169 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en-us">
|
|
3
|
+
<head>
|
|
4
|
+
<!--
|
|
5
|
+
@licstart
|
|
6
|
+
SONIC ROBO BLAST 2
|
|
7
|
+
-----------------------------------------------------------------------------
|
|
8
|
+
Copyright (C) 1993-1996 by id Software, Inc.
|
|
9
|
+
Copyright (C) 1998-2000 by DooM Legacy Team.
|
|
10
|
+
Copyright (C) 1999-2020 by Sonic Team Junior.
|
|
11
|
+
|
|
12
|
+
This program is free software: you can redistribute it and/or modify
|
|
13
|
+
it under the terms of the GNU General Public License as published by
|
|
14
|
+
the Free Software Foundation, either version 2 of the License, or
|
|
15
|
+
(at your option) any later version.
|
|
16
|
+
|
|
17
|
+
This program is distributed in the hope that it will be useful,
|
|
18
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
19
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
20
|
+
GNU General Public License for more details.
|
|
21
|
+
|
|
22
|
+
You should have received a copy of the GNU General Public License
|
|
23
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
24
|
+
@licend
|
|
25
|
+
-->
|
|
26
|
+
<base href="https://raw.githack.com/dskjfoisjfsjio/Standalone-games/main/srb2web-main/">
|
|
27
|
+
<meta charset="utf-8">
|
|
28
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
29
|
+
<link rel="icon" type="image/png" href="/src/assets/new logo.png">
|
|
30
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
31
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
32
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
33
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
|
|
34
|
+
<link rel="apple-touch-icon" href="assets/srb2.png">
|
|
35
|
+
<meta name="apple-mobile-web-app-title" content="SRB2">
|
|
36
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
|
37
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
|
38
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
|
39
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
|
40
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
|
41
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
|
42
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
|
43
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
|
44
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
|
45
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
|
46
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
|
47
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
|
48
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
|
49
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
|
50
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
|
51
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
|
|
52
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
|
53
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
|
54
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
|
55
|
+
<link rel="apple-touch-startup-image" href="assets/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
|
|
56
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.3.0/jszip.min.js"></script>
|
|
57
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.10.1/Sortable.min.js"></script>
|
|
58
|
+
<script src="https://cdn.jsdelivr.net/gh/mazmazz/idb-keyval@idb-version/dist/idb-keyval-iife.min.js"></script>
|
|
59
|
+
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js"></script>
|
|
60
|
+
<script src="https://cdn.jsdelivr.net/npm/es6-promise-pool@2.5.0/es6-promise-pool.min.js"></script>
|
|
61
|
+
<link rel="icon" href="/assets/new%20logo.png">
|
|
62
|
+
|
|
63
|
+
<title>Sonic Robo Blast 2</title>
|
|
64
|
+
<style>
|
|
65
|
+
html, body {
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
body {
|
|
70
|
+
font-family: 'Segoe UI', sans-serif;
|
|
71
|
+
margin: 0;
|
|
72
|
+
padding: none;
|
|
73
|
+
background-color: #000;
|
|
74
|
+
background-image:
|
|
75
|
+
linear-gradient(90deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.80) 10%, rgba(0,0,0,0.80) 90%, rgba(0,0,0,0.25) 100%),
|
|
76
|
+
url(assets/background.jpg);
|
|
77
|
+
background-size: cover;
|
|
78
|
+
background-attachment: fixed;
|
|
79
|
+
background-position: center;
|
|
80
|
+
color: #ffffff;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
body.startedMainLoop {
|
|
84
|
+
background-image: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
body.startedMainLoop #content {
|
|
88
|
+
display: none;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
a {
|
|
92
|
+
color: rgb(107, 161, 255);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
a:visited {
|
|
96
|
+
color: #7f6bd1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media (min-width: 320px) {
|
|
100
|
+
#logo {
|
|
101
|
+
width: 120%;
|
|
102
|
+
height: auto;
|
|
103
|
+
margin: 0 -10%;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#systemArguments, #userArguments {
|
|
107
|
+
width: 120%;
|
|
108
|
+
margin: 0 -10%;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@media (min-width: 481px) {
|
|
113
|
+
#logo {
|
|
114
|
+
width: 90%;
|
|
115
|
+
height: auto;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#systemArguments, #userArguments {
|
|
119
|
+
width: 90%;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@media (min-width: 641px) {
|
|
124
|
+
#logo {
|
|
125
|
+
width: 60%;
|
|
126
|
+
height: auto;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#systemArguments, #userArguments {
|
|
130
|
+
width: 60%;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@media (min-width: 1281px) {
|
|
135
|
+
#logo {
|
|
136
|
+
width: 40%;
|
|
137
|
+
height: auto;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#systemArguments, #userArguments {
|
|
141
|
+
width: 40%;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#status-cont {
|
|
146
|
+
display: inline-block;
|
|
147
|
+
font-weight: bold;
|
|
148
|
+
text-align: center;
|
|
149
|
+
margin: auto 10%;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#progress {
|
|
153
|
+
height: 20px;
|
|
154
|
+
width: 256px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#canvas, #keyCapture {
|
|
158
|
+
position: fixed;
|
|
159
|
+
top: 0;
|
|
160
|
+
left: 0;
|
|
161
|
+
width: 100% !important;
|
|
162
|
+
height: 100% !important;
|
|
163
|
+
opacity: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#keyCapture {
|
|
167
|
+
opacity: 0;
|
|
168
|
+
background-color: rgba(0,0,0,0);
|
|
169
|
+
border: 0;
|
|
170
|
+
color: rgba(0,0,0,0);
|
|
171
|
+
z-index: -99999;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#textForm {
|
|
175
|
+
display: flex;
|
|
176
|
+
position: fixed;
|
|
177
|
+
bottom: 0;
|
|
178
|
+
left: 0;
|
|
179
|
+
width: 100%;
|
|
180
|
+
height: 2.5em;
|
|
181
|
+
z-index: -99999;
|
|
182
|
+
opacity: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#textCapture {
|
|
186
|
+
flex-grow: 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#textSubmit {
|
|
190
|
+
width: 6.66em;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#content {
|
|
194
|
+
width: 100%;
|
|
195
|
+
height: 100%;
|
|
196
|
+
overflow-y: auto;
|
|
197
|
+
text-align: center;
|
|
198
|
+
display: grid;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
button, .button {
|
|
202
|
+
width: 160px;
|
|
203
|
+
height: 48px;
|
|
204
|
+
margin: 0.1em;
|
|
205
|
+
background-color: darkgray;
|
|
206
|
+
background-image: linear-gradient(black, darkgray);
|
|
207
|
+
border: 0.1em gray solid;
|
|
208
|
+
border-radius: 2px;
|
|
209
|
+
color: white;
|
|
210
|
+
font-weight: bold;
|
|
211
|
+
text-align: center;
|
|
212
|
+
line-height: 2.5em;
|
|
213
|
+
display: inline-block;
|
|
214
|
+
cursor: default;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.blueButton {
|
|
218
|
+
background-color: mediumblue;
|
|
219
|
+
background-image: linear-gradient(black, mediumblue);
|
|
220
|
+
border-color: mediumblue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.yellowButton {
|
|
224
|
+
background-color: black;
|
|
225
|
+
background-image: linear-gradient(black, white);
|
|
226
|
+
border-color: white;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.redButton {
|
|
230
|
+
background-image: linear-gradient(black, crimson);
|
|
231
|
+
border-color: crimson;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#startbtn {
|
|
235
|
+
background-color: black;
|
|
236
|
+
color: white;
|
|
237
|
+
background-image: linear-gradient(grey, white);
|
|
238
|
+
border: 0.1em black solid;
|
|
239
|
+
font-weight: bolder;
|
|
240
|
+
font-size: 1.25em;
|
|
241
|
+
line-height: normal;
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#notes {
|
|
246
|
+
display: inline-block;
|
|
247
|
+
text-align: start;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
summary {
|
|
251
|
+
margin: 0.75em 0;
|
|
252
|
+
font-size: 1.2em;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#addonFilesTemplate {
|
|
256
|
+
display: none;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.addonFilesField {
|
|
260
|
+
display: inline-block;
|
|
261
|
+
width: 100%;
|
|
262
|
+
margin: 0.1em 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#resetbtn {
|
|
266
|
+
display: none;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.addonFilesField .addonButton {
|
|
270
|
+
width: 2.5em;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.addonFilesField label,
|
|
274
|
+
.addonFilesField input[type=file] {
|
|
275
|
+
width: 12.5em;
|
|
276
|
+
height: 2.5em;
|
|
277
|
+
margin: 0;
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.addonFilesField input[type=file] {
|
|
282
|
+
font-size: 1em;
|
|
283
|
+
display: none;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.addonFilesField .addonDelete {
|
|
287
|
+
width: 2.5em;
|
|
288
|
+
height: 2.5em;
|
|
289
|
+
margin: 0 0 0 0.1em;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#addonButtonAdd {
|
|
293
|
+
width: 15.4em;
|
|
294
|
+
height: 2.5em;
|
|
295
|
+
margin: 0.1em 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#androidNotice {
|
|
299
|
+
display: none;
|
|
300
|
+
}
|
|
301
|
+
</style>
|
|
302
|
+
</head>
|
|
303
|
+
<body>
|
|
304
|
+
<div id="content">
|
|
305
|
+
<div id="status-cont" class="emscripten">
|
|
306
|
+
<div id="header">
|
|
307
|
+
<p>
|
|
308
|
+
<img id="logo" src="assets/srb2logo.png"/>
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
311
|
+
<div id="details">
|
|
312
|
+
<p>
|
|
313
|
+
<button id="startbtn" onclick="StartLoad()">Play</button>
|
|
314
|
+
<span hidden class="emscripten" id="status"></span>
|
|
315
|
+
</p>
|
|
316
|
+
<progress value="0" max="100" id="progress" hidden=1></progress>
|
|
317
|
+
<p>
|
|
318
|
+
<button id="resetbtn" class="button" onclick="javascript:window.location.reload()">Cancel</button>
|
|
319
|
+
</p>
|
|
320
|
+
</div>
|
|
321
|
+
<form id="controlsForm" action="javascript:void(0);">
|
|
322
|
+
<details id="addons">
|
|
323
|
+
<summary>Mods</summary>
|
|
324
|
+
<div id="addonFilesContainer">
|
|
325
|
+
<div class="addonFilesField" id="addonFilesTemplate">
|
|
326
|
+
<input type="file" id="addonFiles" class="button yellowButton"/>
|
|
327
|
+
<label for="addonFiles" class="button yellowButton">Load File</label><!--
|
|
328
|
+
--><div class="addonButton addonDelete button redButton">-</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div id="addonButtonAdd" class="button blueButton">+</div>
|
|
332
|
+
<p>
|
|
333
|
+
<input type="checkbox" id="addonStartup" checked/>
|
|
334
|
+
<label for="addonStartup">Load Mods on Startup</label>
|
|
335
|
+
</p>
|
|
336
|
+
<details id="addonsHelp">
|
|
337
|
+
<summary>Mod Help</summary>
|
|
338
|
+
<p>You may load one or more mods here. Drag each mod to change its loading order.</p>
|
|
339
|
+
<p>Or, you may load ZIP files. They will be available in the Addons Menu.</p>
|
|
340
|
+
<p>To load savegames and gamedata, place them in a folder titled <code>userdata/</code> within the ZIP file. Your stored gamedata will be overwritten.</p>
|
|
341
|
+
<p><a href="https://mb.srb2.org/forumdisplay.php?f=116" target="_blank">You may download mods here</a>.</p>
|
|
342
|
+
</details>
|
|
343
|
+
</details>
|
|
344
|
+
<details id="controls">
|
|
345
|
+
<summary>Settings</summary>
|
|
346
|
+
<p>
|
|
347
|
+
<label for="resizeHeight">Resolution</label>
|
|
348
|
+
<input type="range" id="resizeHeight" name="resizeHeight" oninput="ShowValue(this, 'p', (elem) => { return elem.value > 800; }, 'Full');"
|
|
349
|
+
min="200" max="900" value="200" step="100"/>
|
|
350
|
+
<span id="resizeHeightValue"></span>
|
|
351
|
+
</p>
|
|
352
|
+
<!--<p id="fullscreenRow">
|
|
353
|
+
<input type="checkbox" id="fullscreen" checked/>
|
|
354
|
+
<label for="fullscreen">Fullscreen</label>
|
|
355
|
+
</p>-->
|
|
356
|
+
<p>
|
|
357
|
+
<input type="checkbox" id="playMusic" checked/>
|
|
358
|
+
<label for="playMusic">Play Music</label>
|
|
359
|
+
<input type="checkbox" id="playSound" checked/>
|
|
360
|
+
<label for="playSound">Play Sounds</label>
|
|
361
|
+
</p>
|
|
362
|
+
<p>
|
|
363
|
+
<input type="checkbox" id="useMouse" checked/>
|
|
364
|
+
<label for="useMouse">Use Mouse</label>
|
|
365
|
+
</p>
|
|
366
|
+
<details id="storageControls">
|
|
367
|
+
<summary>Storage</summary>
|
|
368
|
+
<button class="yellowButton" onclick="DownloadFS(); return false;">Download User Data</button>
|
|
369
|
+
<button class="redButton" onclick="if (confirm('Are you sure? All installed data will be reset!\n\nYour user data will be preserved.')) ResetProgramData(true, _=>alert('Program data reset.')); return false;">Reset Program Data</button>
|
|
370
|
+
<button onclick="if (confirm('Are you sure? All your progress will be lost!')) DeleteFS('/home/web_user/.srb2', true, _=>alert('User data deleted.')); return false;">Delete User Data</button>
|
|
371
|
+
</details>
|
|
372
|
+
<details id="advancedControls">
|
|
373
|
+
<summary>Advanced</summary>
|
|
374
|
+
<p>
|
|
375
|
+
<p>
|
|
376
|
+
<label for="drawDistance">Draw Distance</label>
|
|
377
|
+
<input type="range" id="drawDistance" name="drawDistance" oninput="ShowValueRange(this, '', DrawDistanceRange);"
|
|
378
|
+
min="0" max="10" value="5" step="1"/>
|
|
379
|
+
<span id="drawDistanceValue"></span>
|
|
380
|
+
</p>
|
|
381
|
+
<p>
|
|
382
|
+
<input type="checkbox" id="shadow" checked/>
|
|
383
|
+
<label for="shadow">Render Shadows</label>
|
|
384
|
+
<input type="checkbox" id="midisoundfont" class="nosave"/>
|
|
385
|
+
<label for="midisoundfont">Reset Soundfont</label>
|
|
386
|
+
</p>
|
|
387
|
+
</p>
|
|
388
|
+
<p>
|
|
389
|
+
<span id="packageVersionRow">
|
|
390
|
+
<label for="packageVersion">Version</label>
|
|
391
|
+
<select id="packageVersion">
|
|
392
|
+
<option value="2.2.4" selected>2.2.4</option>
|
|
393
|
+
<option value="2.2.4-lowend">2.2.4-lowend</option>
|
|
394
|
+
</select>
|
|
395
|
+
</span>
|
|
396
|
+
<input type="checkbox" id="lowend"/>
|
|
397
|
+
<label for="lowend">Prefer Low-End</label>
|
|
398
|
+
</p>
|
|
399
|
+
<p>
|
|
400
|
+
<label for="userArguments">Command Line</label><br/>
|
|
401
|
+
<textarea id="systemArguments" rows="2" readonly></textarea><br/>
|
|
402
|
+
<textarea id="userArguments" rows="2" placeholder="Custom Arguments"></textarea>
|
|
403
|
+
</p>
|
|
404
|
+
</details>
|
|
405
|
+
</details>
|
|
406
|
+
</form>
|
|
407
|
+
<details id="help">
|
|
408
|
+
<summary>Help</summary>
|
|
409
|
+
<p>Game Notes:</p>
|
|
410
|
+
<p id="fullscreenNotice">Toggle fullscreen by pressing F11, which should enter your browser's fullscreen mode.</p>
|
|
411
|
+
<p>No online multiplayer: to play netgames, download the PC version at <a href="//www.srb2.org" target="_blank">srb2.org</a>.</p>
|
|
412
|
+
<p>Game controllers should work.</p>
|
|
413
|
+
<p>If this page crashes, try enabling "Low-End" mode under Settings > Advanced.</p>
|
|
414
|
+
<p id="androidNotice">There is a native Android port available on <a href="https://github.com/Jimita/SRB2/releases" target="_blank">GitHub</a>.</p>
|
|
415
|
+
</details>
|
|
416
|
+
<details id="footer">
|
|
417
|
+
<summary>About</summary>
|
|
418
|
+
<p>Sonic Robo Blast 2 is a 3D Sonic the Hedgehog fangame inspired by the original Sonic games of the 1990s.</p>
|
|
419
|
+
<p>This project allows you to play SRB2 on your web browser, including iPhone and iPad.</p>
|
|
420
|
+
<p><a href="https://github.com/mazmazz/SRB2-emscripten/issues">View issues</a> / <a href="https://github.com/mazmazz/SRB2-emscripten">View source</a></p>
|
|
421
|
+
<p>Contributors:</p>
|
|
422
|
+
<p><a href="https://github.com/mazmazz">mazmazz</a> / <a href="https://github.com/heyjoeway">heyjoeway</a> / <a href="https://github.com/Jimita">Jimita</a></p>
|
|
423
|
+
<p>Follow SRB2:</p>
|
|
424
|
+
<p><a href="https://www.srb2.org">Website</a> / <a href="https://twitter.com/SonicTeamJr">Twitter</a> / <a href="https://discord.com/invite/pYDXzpX">Discord</a></p>
|
|
425
|
+
<p style="font-size:xx-small;">
|
|
426
|
+
SRB2 Web Version: 1592091069
|
|
427
|
+
</p>
|
|
428
|
+
<span id="newVersion"></span>
|
|
429
|
+
<p style="font-size:xx-small;">
|
|
430
|
+
This software is licensed under GNU General Public License Version 2. See license text at <a href="https://opensource.org/licenses/GPL-2.0" target="_blank">https://opensource.org/licenses/GPL-2.0</a>.
|
|
431
|
+
</p>
|
|
432
|
+
<p style="font-size:xx-small;">
|
|
433
|
+
The <a href="https://musical-artifacts.com/artifacts/400" target="_blank">Florestan Basic GM GS</a> soundfont is created by <a href="http://dev.nando.audio" target="_blank">Nando Florestan</a>.
|
|
434
|
+
</p>
|
|
435
|
+
<p style="font-size:xx-small;">
|
|
436
|
+
Original design and content is copyright 1998-2020 Sonic Team Junior. All non-original material is copyrighted by their respective owners, and no copyright infringement is intended.
|
|
437
|
+
</p>
|
|
438
|
+
</details>
|
|
439
|
+
<div id="copyright">
|
|
440
|
+
<p style="font-size:xx-small;">
|
|
441
|
+
Sonic Robo Blast 2 and Sonic Team Junior are in no way affiliated with SEGA® or Sonic Team.
|
|
442
|
+
</p>
|
|
443
|
+
<p style="font-size:xx-small;">
|
|
444
|
+
Sonic the Hedgehog is a trademark of SEGA®<br/>
|
|
445
|
+
The copyrights of "Sonic the Hedgehog" and all associated characters, names, terms, art, and music thereof belong to SEGA®
|
|
446
|
+
</p>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1" style="z-index:-9999;"></canvas>
|
|
452
|
+
|
|
453
|
+
<textarea id="keyCapture" oncontextmenu="event.preventDefault()" tabindex="-1"></textarea>
|
|
454
|
+
|
|
455
|
+
<form id="textForm" onsubmit="InjectText();" action="javascript:void(0);" autocomplete="off">
|
|
456
|
+
<input id="textCapture" tabindex="-1" placeholder="Console Command" disabled/>
|
|
457
|
+
<input id="textSubmit" type="submit" value="Enter"/>
|
|
458
|
+
</form>
|
|
459
|
+
|
|
460
|
+
<script type='text/javascript'>
|
|
461
|
+
////////////////////////////////
|
|
462
|
+
// UI
|
|
463
|
+
////////////////////////////////
|
|
464
|
+
|
|
465
|
+
var CanvasElement = document.getElementById('canvas');
|
|
466
|
+
var StatusElement = document.getElementById('status');
|
|
467
|
+
var ProgressElement = document.getElementById('progress');
|
|
468
|
+
var ControlsFormElement = document.getElementById('controlsForm');
|
|
469
|
+
var PackageVersionElement = document.getElementById('packageVersion');
|
|
470
|
+
var AddonFields = 0;
|
|
471
|
+
|
|
472
|
+
HTMLSelectElement.prototype.contains = function( value ) {
|
|
473
|
+
for ( var i = 0, l = this.options.length; i < l; i++ ) {
|
|
474
|
+
if ( this.options[i].value == value ) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
var URLParams;
|
|
482
|
+
|
|
483
|
+
var GetSelectedPackageVersion = _ => {
|
|
484
|
+
let val = '';
|
|
485
|
+
if (PackageVersionElement.selectedIndex > -1) {
|
|
486
|
+
val = PackageVersionElement.options[PackageVersionElement.selectedIndex].value;
|
|
487
|
+
if (document.getElementById('lowend').checked
|
|
488
|
+
&& PackageVersionElement.contains(`${val}-lowend`))
|
|
489
|
+
val = `${val}-lowend`;
|
|
490
|
+
} else
|
|
491
|
+
val = '2.2.4';
|
|
492
|
+
return val;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
var DeleteNode = (node) => {
|
|
496
|
+
node.querySelectorAll('*').forEach(n => n.remove());
|
|
497
|
+
node.remove();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
var HideContent = () => {
|
|
501
|
+
StatusElement.hidden = false;
|
|
502
|
+
ProgressElement.hidden = false;
|
|
503
|
+
document.getElementById('startbtn').style.display = "none";
|
|
504
|
+
document.getElementById('addons').style.display = "none";
|
|
505
|
+
document.getElementById('controls').style.display = "none";
|
|
506
|
+
document.getElementById('help').style.display = "none";
|
|
507
|
+
document.getElementById('footer').style.display = "none";
|
|
508
|
+
document.getElementById('copyright').style.display = "none";
|
|
509
|
+
document.getElementById('resetbtn').style.display = "inline-block";
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
var ShowContent = () => {
|
|
513
|
+
StatusElement.hidden = true;
|
|
514
|
+
ProgressElement.hidden = true;
|
|
515
|
+
document.getElementById('startbtn').style.display = "inline-block";
|
|
516
|
+
document.getElementById('addons').style.display = "block";
|
|
517
|
+
document.getElementById('controls').style.display = "block";
|
|
518
|
+
document.getElementById('help').style.display = "block";
|
|
519
|
+
document.getElementById('footer').style.display = "block";
|
|
520
|
+
document.getElementById('copyright').style.display = "block";
|
|
521
|
+
document.getElementById('resetbtn').style.display = "none";
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
var StartLoad = async () => {
|
|
525
|
+
PackageVersion = GetSelectedPackageVersion();
|
|
526
|
+
|
|
527
|
+
SaveFormToStorage(ControlsFormElement);
|
|
528
|
+
window.removeEventListener('focus', CheckVersion, false);
|
|
529
|
+
await UXInstallProgram(false, PackageVersion); // hides the UI
|
|
530
|
+
await GetWasm(PackageVersion);
|
|
531
|
+
let scriptSrc = `data/${PackageVersion}/srb2.js`;
|
|
532
|
+
let script = document.createElement('script');
|
|
533
|
+
script.setAttribute('src',scriptSrc);
|
|
534
|
+
document.head.appendChild(script);
|
|
535
|
+
document.body.classList.add('calledRun');
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
var DrawDistanceRange = ['256','512','768','1024','1536','2048','3072','4096','6144','8192','Infinite'];
|
|
539
|
+
|
|
540
|
+
var ShowValueRange = (elem, suffix, rangeValues) => {
|
|
541
|
+
document.getElementById(`${elem.id}Value`).innerText = `${rangeValues[elem.value]}${suffix}`;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
var ShowValue = (elem, suffix, overrideTest, override) => {
|
|
545
|
+
if (typeof overrideTest !== 'undefined' && overrideTest(elem))
|
|
546
|
+
document.getElementById(`${elem.id}Value`).innerText = override;
|
|
547
|
+
else
|
|
548
|
+
document.getElementById(`${elem.id}Value`).innerText = `${elem.value}${suffix}`;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
var SaveFormToStorage = (form) => {
|
|
552
|
+
let inputs = form.querySelectorAll('input, textarea, select');
|
|
553
|
+
inputs.forEach(input => {
|
|
554
|
+
let val = input.type === 'checkbox' ? input.checked
|
|
555
|
+
: input.type === 'select-one' ? (input.selectedIndex > -1 ? input.options[input.selectedIndex].value : '')
|
|
556
|
+
: input.value
|
|
557
|
+
// Don't save the field if it's specified in the URL query string,
|
|
558
|
+
// because that's a forced override.
|
|
559
|
+
if (input.type !== 'file' && (!URLParams.has(input.id) || URLParams.get(input.id) != val) && !input.classList.contains('nosave'))
|
|
560
|
+
localStorage.setItem(`${form.id}_${input.id}`, val);
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
var LoadFormFromStorage = (form) => {
|
|
565
|
+
let inputs = form.querySelectorAll('input, textarea, select');
|
|
566
|
+
inputs.forEach(input => {
|
|
567
|
+
let val;
|
|
568
|
+
if(URLParams.has(input.id))
|
|
569
|
+
val = URLParams.get(input.id);
|
|
570
|
+
else
|
|
571
|
+
val = localStorage.getItem(`${form.id}_${input.id}`);
|
|
572
|
+
if (val !== null && !input.readOnly && input.type !== 'file' && !input.classList.contains('nosave')) {
|
|
573
|
+
if (input.type === 'checkbox')
|
|
574
|
+
input.checked = (val === "true");
|
|
575
|
+
else if (input.type === 'select-one')
|
|
576
|
+
for (let i = 0; i < input.length; i++) {
|
|
577
|
+
let option = input.options[i];
|
|
578
|
+
if (option.value === val) {
|
|
579
|
+
input.selectedIndex = i;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else
|
|
584
|
+
input.value = val;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
var HandleInput = (e) => {
|
|
590
|
+
let val = e.target.type === 'checkbox' ? e.target.checked
|
|
591
|
+
: e.target.type === 'select-one' ? (e.target.selectedIndex > -1 ? e.target.options[e.target.selectedIndex].value : '')
|
|
592
|
+
: e.target.value
|
|
593
|
+
// Don't save the field if it's specified in the URL query string,
|
|
594
|
+
// because that's a forced override.
|
|
595
|
+
if (e.target.type !== 'file' && (!URLParams.has(e.target.id) || URLParams.get(e.target.id) != val) && !e.target.classList.contains('nosave'))
|
|
596
|
+
localStorage.setItem(`${ControlsFormElement.id}_${e.target.id}`, val);
|
|
597
|
+
BuildControlArguments();
|
|
598
|
+
document.getElementById("systemArguments").value = SystemArgumentsToString();
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
var AddFormEventListeners = (form, event, callback, settings) => {
|
|
602
|
+
let inputs = form.querySelectorAll('input, textarea, select');
|
|
603
|
+
inputs.forEach(input => input.addEventListener(event, callback, settings));
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
var DeleteAddonField = (elem) => {
|
|
607
|
+
DeleteNode(elem);
|
|
608
|
+
if (!document.getElementById("addonFilesContainer").querySelectorAll('.addonFilesField:not([id=addonFilesTemplate])').length)
|
|
609
|
+
AddAddonField();
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
var HandleDeleteAddon = (e) => {
|
|
613
|
+
DeleteAddonField(e.target.parentElement);
|
|
614
|
+
BuildControlArguments();
|
|
615
|
+
document.getElementById("systemArguments").value = SystemArgumentsToString();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
var HandleFileInput = (e) => {
|
|
619
|
+
if (e.target.files.length) {
|
|
620
|
+
let name = e.target.files[0].name;
|
|
621
|
+
// truncate name ourselves, because overflow: hidden bugs the layout
|
|
622
|
+
if (name.length > 16)
|
|
623
|
+
name = `${name.substring(0, 16)}...`;
|
|
624
|
+
e.target.parentElement.querySelector('label').innerText = name;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
var AddAddonField = () => {
|
|
629
|
+
let newField = document.getElementById("addonFilesTemplate").cloneNode(true);
|
|
630
|
+
newField.id = `addonFilesField${AddonFields}`;
|
|
631
|
+
newField.querySelector('input[type=file]').id = `addonFiles${AddonFields}`;
|
|
632
|
+
newField.querySelector('input[type=file]').addEventListener('input', HandleFileInput, false);
|
|
633
|
+
newField.querySelector('input[type=file]').addEventListener('input', HandleInput, false);
|
|
634
|
+
newField.querySelector('label').htmlFor = `addonFiles${AddonFields}`;
|
|
635
|
+
// iOS file inputs do not emit input events, so let's expose
|
|
636
|
+
// the input element itself so the user can see the filename.
|
|
637
|
+
if (UserAgentIsiOS()) {
|
|
638
|
+
newField.querySelector('label').style.display = 'none';
|
|
639
|
+
newField.querySelector('input[type=file]').classList.add('addonButton');
|
|
640
|
+
newField.querySelector('input[type=file]').style.display = 'inline-block';
|
|
641
|
+
}
|
|
642
|
+
newField.querySelector('.addonDelete').addEventListener('click', HandleDeleteAddon, false);
|
|
643
|
+
newField.style.display = 'block';
|
|
644
|
+
document.getElementById("addonFilesContainer").insertBefore(newField, null);
|
|
645
|
+
AddonFields++;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
var InitializeiOSLanding = () => {
|
|
649
|
+
if (UserAgentIsiOS()) {
|
|
650
|
+
if (!IsStandalone()) {
|
|
651
|
+
document.getElementById("details").innerHTML = "<p style='font-weight: 900;'>To play, add this site to your Home Screen!</p>";
|
|
652
|
+
document.getElementById("addons").style.display = "none";
|
|
653
|
+
document.getElementById("controls").style.display = "none";
|
|
654
|
+
document.getElementById("help").style.display = "none";
|
|
655
|
+
document.getElementById("notes").style.display = "none";
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
var InitializeFormFields = () => {
|
|
662
|
+
URLParams = new URLSearchParams(window.location.search);
|
|
663
|
+
|
|
664
|
+
LoadFormFromStorage(ControlsFormElement);
|
|
665
|
+
|
|
666
|
+
// By default, mobile devices should run at 200p.
|
|
667
|
+
if (localStorage.getItem('controlsForm_resizeHeight') === null && !UserAgentIsMobile())
|
|
668
|
+
document.getElementById('resizeHeight').value = 400;
|
|
669
|
+
|
|
670
|
+
// iOS devices default to low-end builds. the high-end build does not play
|
|
671
|
+
// even on new devices
|
|
672
|
+
if (UserAgentIsiOS()) {
|
|
673
|
+
document.getElementById('lowend').checked = true;
|
|
674
|
+
document.getElementById('lowend').disabled = true;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
ShowValue(document.getElementById('resizeHeight'), 'p', (elem) => { return elem.value > 800; }, 'Full');
|
|
678
|
+
|
|
679
|
+
ShowValueRange(document.getElementById('drawDistance'), '', DrawDistanceRange);
|
|
680
|
+
|
|
681
|
+
AddFormEventListeners(ControlsFormElement, 'input', HandleInput, false);
|
|
682
|
+
|
|
683
|
+
if (PackageVersionElement.length < 2)
|
|
684
|
+
document.getElementById('packageVersionRow').style.display = 'none';
|
|
685
|
+
|
|
686
|
+
BuildControlArguments();
|
|
687
|
+
document.getElementById("systemArguments").value = SystemArgumentsToString();
|
|
688
|
+
|
|
689
|
+
if (UserAgentIsAndroid())
|
|
690
|
+
document.getElementById("androidNotice").style.display = "block";
|
|
691
|
+
|
|
692
|
+
if (UserAgentIsMobile()) {
|
|
693
|
+
if (IsStandalone())
|
|
694
|
+
document.getElementById("fullscreenNotice").style.display = "none";
|
|
695
|
+
else
|
|
696
|
+
document.getElementById("fullscreenNotice").innerText = "To play in fullscreen, try adding this web page to your Home Screen.";
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (UserAgentIsiOS() && IsStandalone()) {
|
|
700
|
+
document.getElementById("fullscreenRow").style.display = "none";
|
|
701
|
+
document.getElementById("fullscreen").checked = true;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
var InitializeAddons = () => {
|
|
706
|
+
AddAddonField();
|
|
707
|
+
document.getElementById("addonButtonAdd").addEventListener('click', AddAddonField, false);
|
|
708
|
+
Sortable.create(document.getElementById('addonFilesContainer'), {
|
|
709
|
+
onEnd: () => {
|
|
710
|
+
BuildControlArguments();
|
|
711
|
+
document.getElementById("systemArguments").value = SystemArgumentsToString();
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
var InitializeSections = () => {
|
|
717
|
+
document.getElementById('addons').open = (localStorage.getItem('addons') === 'true');
|
|
718
|
+
document.getElementById('addonsHelp').open = (localStorage.getItem('addonsHelp') === 'true'
|
|
719
|
+
|| localStorage.getItem('addonsHelp') === null);
|
|
720
|
+
document.getElementById('controls').open = (localStorage.getItem('controls') === 'true');
|
|
721
|
+
document.getElementById('advancedControls').open = (localStorage.getItem('advancedControls') === 'true');
|
|
722
|
+
|
|
723
|
+
document.getElementById('addons').addEventListener('toggle', function(e) {
|
|
724
|
+
localStorage.setItem('addons', event.target.open);
|
|
725
|
+
}, false);
|
|
726
|
+
|
|
727
|
+
document.getElementById('addonsHelp').addEventListener('toggle', function(e) {
|
|
728
|
+
localStorage.setItem('addonsHelp', event.target.open);
|
|
729
|
+
}, false);
|
|
730
|
+
|
|
731
|
+
document.getElementById('controls').addEventListener('toggle', function(e) {
|
|
732
|
+
localStorage.setItem('controls', event.target.open);
|
|
733
|
+
}, false);
|
|
734
|
+
|
|
735
|
+
document.getElementById('advancedControls').addEventListener('toggle', function(e) {
|
|
736
|
+
localStorage.setItem('advancedControls', event.target.open);
|
|
737
|
+
}, false);
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
window.addEventListener('load', function() {
|
|
741
|
+
if (!InitializeiOSLanding()) {
|
|
742
|
+
InitializeAddons();
|
|
743
|
+
InitializeFormFields();
|
|
744
|
+
InitializeSections();
|
|
745
|
+
}
|
|
746
|
+
}, {once: true});
|
|
747
|
+
|
|
748
|
+
////////////////////////////////
|
|
749
|
+
// Version Check - For PWA
|
|
750
|
+
////////////////////////////////
|
|
751
|
+
|
|
752
|
+
let AuthorUpdateShown = false;
|
|
753
|
+
|
|
754
|
+
var CheckVersion = async () => {
|
|
755
|
+
if (document.body.classList.contains('calledRun'))
|
|
756
|
+
return;
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
////////////////////////////////
|
|
760
|
+
// Check Web Version
|
|
761
|
+
////////////////////////////////
|
|
762
|
+
|
|
763
|
+
let response = await fetch("version-shell.txt");
|
|
764
|
+
let data = await response.text();
|
|
765
|
+
|
|
766
|
+
console.log(`SRB2 Web Version: 1592091069`);
|
|
767
|
+
if (data.trim() && data.trim() !== "1592091069") {
|
|
768
|
+
console.log(`New Web Version Found: ${data}`);
|
|
769
|
+
// Don't refresh page more than once in a row.
|
|
770
|
+
let updateCount = localStorage.getItem('srb2web_update');
|
|
771
|
+
if (IsStandalone() && (!updateCount || !parseInt(updateCount))) {
|
|
772
|
+
localStorage.setItem('srb2web_update', (1).toString());
|
|
773
|
+
window.location.reload(true);
|
|
774
|
+
return;
|
|
775
|
+
} else {
|
|
776
|
+
if (document.getElementById('newVersion'))
|
|
777
|
+
document.getElementById('newVersion').outerHTML = `<p style="font-size:xx-small;">Update Version: <a href="#" onclick="window.location.reload(true);return false;">${data}</a></p>`;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
localStorage.setItem('srb2web_update', (0).toString());
|
|
781
|
+
|
|
782
|
+
////////////////////////////////
|
|
783
|
+
// Check Author Updates
|
|
784
|
+
////////////////////////////////
|
|
785
|
+
|
|
786
|
+
// If this app were to be decentralized, I would still to be able
|
|
787
|
+
// to communicate updates. Line 1 is the update ID, everything else
|
|
788
|
+
// goes in an alert. Show the alert on two page loads.
|
|
789
|
+
// Put new updates in a new filename.
|
|
790
|
+
|
|
791
|
+
if (!AuthorUpdateShown) {
|
|
792
|
+
try {
|
|
793
|
+
// get data
|
|
794
|
+
let authorUrl = 'https://raw.githubusercontent.com/mazmazz/SRB2-emscripten/emscripten-new/emscripten/author-update.txt';
|
|
795
|
+
let authorResponse = await fetch(authorUrl, {mode: 'cors'});
|
|
796
|
+
if (!authorResponse.ok)
|
|
797
|
+
throw(`Author update check status: ${authorResponse.status}`);
|
|
798
|
+
data = await authorResponse.text();
|
|
799
|
+
data = data.replace('\r\n','\n');
|
|
800
|
+
let linePos = data.indexOf('\n');
|
|
801
|
+
|
|
802
|
+
// process data
|
|
803
|
+
let updateId = data.substr(0, linePos > -1 ? linePos : data.length);
|
|
804
|
+
let updateText = data.substr(linePos+1 < data.length ? linePos+1 : data.length, data.length);
|
|
805
|
+
|
|
806
|
+
if (!updateId.trim() || !updateText.trim())
|
|
807
|
+
throw('Invalid author update, updateId or updateText was blank');
|
|
808
|
+
|
|
809
|
+
// compare against stored info
|
|
810
|
+
let updateCount = localStorage.getItem('srb2web_authorUpdate');
|
|
811
|
+
updateCount = parseInt(updateCount);
|
|
812
|
+
if (!updateCount)
|
|
813
|
+
updateCount = 0;
|
|
814
|
+
let updateStoredId = localStorage.getItem('srb2web_authorUpdate_id');
|
|
815
|
+
if (updateId !== updateStoredId)
|
|
816
|
+
updateCount = 0;
|
|
817
|
+
|
|
818
|
+
// show the update?
|
|
819
|
+
if (!AuthorUpdateShown) {
|
|
820
|
+
if (updateCount++ < 2) {
|
|
821
|
+
AuthorUpdateShown = true; // don't show again for this page load
|
|
822
|
+
alert(updateText);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// store variables
|
|
827
|
+
localStorage.setItem('srb2web_authorUpdate_id', updateId);
|
|
828
|
+
localStorage.setItem('srb2web_authorUpdate', updateCount.toString());
|
|
829
|
+
} catch (err) {
|
|
830
|
+
console.log('CheckVersion: author update check', err);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
////////////////////////////////
|
|
835
|
+
// Check default program version
|
|
836
|
+
////////////////////////////////
|
|
837
|
+
|
|
838
|
+
// Don't check program version more than once a page load
|
|
839
|
+
let updateCount = localStorage.getItem('srb2program_update');
|
|
840
|
+
if (!updateCount || !parseInt(updateCount))
|
|
841
|
+
response = await fetch("version-package.txt");
|
|
842
|
+
else
|
|
843
|
+
throw("Already checked program version.");
|
|
844
|
+
|
|
845
|
+
data = await response.text();
|
|
846
|
+
|
|
847
|
+
let previousDefaultPackageVersion = localStorage.getItem('srb2program_defaultversion');
|
|
848
|
+
console.log(`SRB2 Default Program Version: 2.2.4`);
|
|
849
|
+
if (data.trim() && previousDefaultPackageVersion && data.trim() !== previousDefaultPackageVersion) {
|
|
850
|
+
console.log(`New Default Program Version Found: ${data}`);
|
|
851
|
+
if (PackageVersionElement.length > 1 && confirm(`SRB2 Version ${data} is released! Do you want to switch to the new version?`)) {
|
|
852
|
+
for (let i = 0; i < PackageVersionElement.length; i++) {
|
|
853
|
+
if (PackageVersionElement.options[i].value === data) {
|
|
854
|
+
PackageVersionElement.selectedIndex = i;
|
|
855
|
+
SaveFormToStorage(ControlsFormElement);
|
|
856
|
+
alert(`The new version will run when you ${UserAgentIsMobile() ? 'tap' : 'click'} "Play".`);
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
localStorage.setItem('srb2program_defaultversion', data.trim());
|
|
863
|
+
} catch (err) {
|
|
864
|
+
console.log(`Error Checking New Version:`,err);
|
|
865
|
+
localStorage.setItem('srb2web_update', (0).toString());
|
|
866
|
+
throw err;
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// reset on page load
|
|
871
|
+
localStorage.setItem('srb2program_update', (0).toString());
|
|
872
|
+
window.addEventListener('load', CheckVersion, {once: true});
|
|
873
|
+
window.addEventListener('focus', CheckVersion, false);
|
|
874
|
+
|
|
875
|
+
////////////////////////////////
|
|
876
|
+
// Utilities
|
|
877
|
+
////////////////////////////////
|
|
878
|
+
|
|
879
|
+
var UserAgentIsiOS = () => {
|
|
880
|
+
var ua = window.navigator.userAgent;
|
|
881
|
+
var iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i) || !!ua.match(/iPod/i);
|
|
882
|
+
var webkit = !!ua.match(/WebKit/i);
|
|
883
|
+
var iOSSafari = iOS && webkit && !ua.match(/CriOS/i) && !ua.match(/FxiOS/i);
|
|
884
|
+
return iOSSafari;
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
var UserAgentIsiPhone = () => /iPhone|iPod/.test(navigator.userAgent);
|
|
888
|
+
|
|
889
|
+
var IsStandalone = () => (
|
|
890
|
+
(window.matchMedia('(display-mode: standalone)').matches) ||
|
|
891
|
+
(("standalone" in window.navigator) &&
|
|
892
|
+
window.navigator.standalone)
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
// https://stackoverflow.com/a/11381730
|
|
896
|
+
var UserAgentIsMobile = () => {
|
|
897
|
+
let check = false;
|
|
898
|
+
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
|
|
899
|
+
return check;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
var UserAgentIsAndroid = () => /Android/.test(navigator.userAgent);
|
|
903
|
+
|
|
904
|
+
var ResizeDimensions = (x, y, resizeHeight) => {
|
|
905
|
+
let portrait = (x < y);
|
|
906
|
+
let width = Math.max(x, y);
|
|
907
|
+
let height = Math.min(x, y);
|
|
908
|
+
let target = Math.max(resizeHeight, (portrait ? 320 : 200)); // BASEVIDHEIGHT
|
|
909
|
+
let factor;
|
|
910
|
+
|
|
911
|
+
if (!resizeHeight)
|
|
912
|
+
return {x:x, y:y};
|
|
913
|
+
|
|
914
|
+
factor = target / height;
|
|
915
|
+
if (width * factor < 320) // BASEVIDWIDTH
|
|
916
|
+
factor = 320 / width;
|
|
917
|
+
|
|
918
|
+
width *= factor;
|
|
919
|
+
height *= factor;
|
|
920
|
+
|
|
921
|
+
if (portrait)
|
|
922
|
+
{
|
|
923
|
+
x = Math.ceil(height);
|
|
924
|
+
y = Math.ceil(width);
|
|
925
|
+
}
|
|
926
|
+
else
|
|
927
|
+
{
|
|
928
|
+
x = Math.ceil(width);
|
|
929
|
+
y = Math.ceil(height);
|
|
930
|
+
}
|
|
931
|
+
return {x:x, y:y};
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
////////////////////////////////
|
|
935
|
+
// Base FS Functions
|
|
936
|
+
////////////////////////////////
|
|
937
|
+
|
|
938
|
+
var GetBasenameFromPath = (path) => path.split('\\').pop().split('/').pop();
|
|
939
|
+
var GetDirnameFromPath = (path) => {
|
|
940
|
+
let arr = path.split('\\').pop().split('/');
|
|
941
|
+
arr.pop();
|
|
942
|
+
return arr.join('/');
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
let TypedArrayToBuffer = (array) => array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset);
|
|
946
|
+
|
|
947
|
+
// iOS Safari does not implement Blob.arrayBuffer(), so let's
|
|
948
|
+
// do it ourselves.
|
|
949
|
+
// https://gist.github.com/hanayashiki/8dac237671343e7f0b15de617b0051bd
|
|
950
|
+
function MyArrayBuffer () {
|
|
951
|
+
// this: File or Blob
|
|
952
|
+
return new Promise((resolve) => {
|
|
953
|
+
let fr = new FileReader();
|
|
954
|
+
fr.onload = () => {
|
|
955
|
+
resolve(fr.result);
|
|
956
|
+
};
|
|
957
|
+
fr.readAsArrayBuffer(this);
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
var ImplementArrayBuffer = () => {
|
|
962
|
+
if ('File' in self)
|
|
963
|
+
File.prototype.arrayBuffer = File.prototype.arrayBuffer || MyArrayBuffer;
|
|
964
|
+
if ('Blob' in self)
|
|
965
|
+
Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || MyArrayBuffer;
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
ImplementArrayBuffer();
|
|
969
|
+
|
|
970
|
+
var InitializeFS = () => {
|
|
971
|
+
FS.mkdirTree('/addons');
|
|
972
|
+
FS.symlink('/home/web_user/.srb2', '/addons/.srb2');
|
|
973
|
+
FS.symlink('/home/web_user/.srb2', '/addons/userdata');
|
|
974
|
+
FS.mount(IDBFS, {}, '/home/web_user');
|
|
975
|
+
return (new Promise((resolve, reject) => {
|
|
976
|
+
FS.syncfs(true, (err) => {
|
|
977
|
+
console.log("SyncFS done");
|
|
978
|
+
console.log(err);
|
|
979
|
+
resolve();
|
|
980
|
+
});
|
|
981
|
+
}));
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
var WriteFS = (baseDir, path, data) => {
|
|
985
|
+
if (data instanceof ArrayBuffer)
|
|
986
|
+
data = new Uint8Array(data);
|
|
987
|
+
// split path to base dir and filename
|
|
988
|
+
if (path.includes('/') || path.includes('\\'))
|
|
989
|
+
baseDir = `${baseDir}/${GetDirnameFromPath(path)}`;
|
|
990
|
+
fn = GetBasenameFromPath(path);
|
|
991
|
+
// check for symlinks
|
|
992
|
+
let parents = '/';
|
|
993
|
+
baseDir.split('/').forEach((name) => {
|
|
994
|
+
if (name.length) {
|
|
995
|
+
let mode = 0;
|
|
996
|
+
try { mode = FS.lstat(`${parents}${name}`)['mode']; } catch(err) { console.log('WriteFS(): lstat info'); console.log(err); }
|
|
997
|
+
if (FS.isLink(mode))
|
|
998
|
+
parents = `${FS.readlink(`${parents}${name}`)}/`;
|
|
999
|
+
else
|
|
1000
|
+
parents += `${name}/`;
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
baseDir = parents.substring(0, parents.length-1);
|
|
1004
|
+
console.log(`WriteFS(): Writing ${baseDir}/${fn}: ${data.byteLength} bytes`);
|
|
1005
|
+
// attempt write
|
|
1006
|
+
try { FS.mkdirTree(parents); } catch(err) { console.log('WriteFS(): mkdirTree info'); console.log(err); }
|
|
1007
|
+
try { FS.unlink(`${baseDir}/${fn}`); } catch(err) { console.log('WriteFS(): unlink info'); console.log(err); }
|
|
1008
|
+
FS.createDataFile(baseDir, fn, data, true, true);
|
|
1009
|
+
return Promise.resolve(true);
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
var SyncFS = (populate = false) => {
|
|
1013
|
+
// commit to persistent storage
|
|
1014
|
+
return (new Promise((resolve, reject) => {
|
|
1015
|
+
if (typeof FS !== 'undefined')
|
|
1016
|
+
FS.syncfs(populate, (err) => {
|
|
1017
|
+
console.log('Synced persistent storage');
|
|
1018
|
+
console.log(err);
|
|
1019
|
+
});
|
|
1020
|
+
return resolve();
|
|
1021
|
+
}));
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
var DownloadFS = (downloadPath, manageLoop = true, callback = null) => {
|
|
1025
|
+
// Emscripten file store is an IndexedDB, name /home/web_user, object FILE_DATA,
|
|
1026
|
+
// version 21.
|
|
1027
|
+
// idb-keyval doesn't take in a version parameter (forces 1),
|
|
1028
|
+
// so use mazmazz/idb-keyval@idb-version to specify a version.
|
|
1029
|
+
// Files are listed as [fullPath]: {timestamp, mode, contents}
|
|
1030
|
+
let customStore = new idbKeyval.Store('/home/web_user', 'FILE_DATA', 21);
|
|
1031
|
+
if (typeof downloadPath === 'undefined')
|
|
1032
|
+
downloadPath = '/home/web_user/.srb2';
|
|
1033
|
+
|
|
1034
|
+
if (manageLoop && StartedMainLoop)
|
|
1035
|
+
PauseLoop();
|
|
1036
|
+
|
|
1037
|
+
return SyncFS()
|
|
1038
|
+
.then(_ => {
|
|
1039
|
+
return idbKeyval.keys(customStore);
|
|
1040
|
+
})
|
|
1041
|
+
.then(keys => {
|
|
1042
|
+
let promises = [];
|
|
1043
|
+
let zip = new JSZip();
|
|
1044
|
+
keys.forEach(key => {
|
|
1045
|
+
if (key.includes(downloadPath))
|
|
1046
|
+
promises.push(
|
|
1047
|
+
idbKeyval.get(key, customStore)
|
|
1048
|
+
.then(val => {
|
|
1049
|
+
// JSZip creates nested folders automatically.
|
|
1050
|
+
// Just iterate over files.
|
|
1051
|
+
if ('contents' in val && val.contents)
|
|
1052
|
+
return zip.file(key.replace('/home/web_user/',''), TypedArrayToBuffer(val.contents), {date: new Date(val.timestamp)});
|
|
1053
|
+
})
|
|
1054
|
+
);
|
|
1055
|
+
});
|
|
1056
|
+
return Promise.all(promises)
|
|
1057
|
+
.then(_ => zip);
|
|
1058
|
+
})
|
|
1059
|
+
.then(zip => zip.generateAsync({type:'blob'}))
|
|
1060
|
+
.then(blob => Promise.resolve(saveAs(blob, 'srb2-data.zip')))
|
|
1061
|
+
.catch(err => console.log(`DownloadFS: ${err}`))
|
|
1062
|
+
.finally(_ => {
|
|
1063
|
+
if (typeof callback === 'function')
|
|
1064
|
+
callback();
|
|
1065
|
+
if (manageLoop && StartedMainLoop)
|
|
1066
|
+
ResumeLoop();
|
|
1067
|
+
});
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
var DeleteFS = (deletePath, manageLoop = true, callback = null) => {
|
|
1071
|
+
// Emscripten file store is an IndexedDB, name /home/web_user, object FILE_DATA,
|
|
1072
|
+
// version 21.
|
|
1073
|
+
// idb-keyval doesn't take in a version parameter (forces 1),
|
|
1074
|
+
// so use mazmazz/idb-keyval@idb-version to specify a version.
|
|
1075
|
+
// Files are listed as [fullPath]: {timestamp, mode, contents}
|
|
1076
|
+
if (typeof deletePath === 'undefined')
|
|
1077
|
+
throw 'DeleteFS: Must specify a path';
|
|
1078
|
+
|
|
1079
|
+
if (FS) {
|
|
1080
|
+
try {
|
|
1081
|
+
FS.unlink(`${deletePath}`);
|
|
1082
|
+
} catch (e) {
|
|
1083
|
+
console.error(`DeleteFS: ${deletePath} - `, e);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return SyncFS().then(_ => {
|
|
1087
|
+
if (typeof callback === 'function')
|
|
1088
|
+
callback();
|
|
1089
|
+
if (manageLoop && StartedMainLoop)
|
|
1090
|
+
ResumeLoop();
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
let customStore = new idbKeyval.Store('/home/web_user', 'FILE_DATA', 21);
|
|
1095
|
+
|
|
1096
|
+
if (manageLoop && StartedMainLoop)
|
|
1097
|
+
PauseLoop();
|
|
1098
|
+
|
|
1099
|
+
return SyncFS()
|
|
1100
|
+
.then(_ => {
|
|
1101
|
+
return idbKeyval.keys(customStore);
|
|
1102
|
+
})
|
|
1103
|
+
.then(keys => {
|
|
1104
|
+
let promises = [];
|
|
1105
|
+
keys.forEach(key => {
|
|
1106
|
+
if (key.includes(deletePath))
|
|
1107
|
+
promises.push(idbKeyval.del(key, customStore));
|
|
1108
|
+
});
|
|
1109
|
+
return Promise.all(promises)
|
|
1110
|
+
})
|
|
1111
|
+
.then(_ => SyncFS(true)) // commit changes to the memory FS
|
|
1112
|
+
.catch(err => console.log(`DeleteFS: ${err}`))
|
|
1113
|
+
.finally(_ => {
|
|
1114
|
+
if (typeof callback === 'function')
|
|
1115
|
+
callback();
|
|
1116
|
+
if (manageLoop && StartedMainLoop)
|
|
1117
|
+
ResumeLoop();
|
|
1118
|
+
});
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
////////////////////////////////
|
|
1122
|
+
// Program Data
|
|
1123
|
+
////////////////////////////////
|
|
1124
|
+
|
|
1125
|
+
// Files to download on first run
|
|
1126
|
+
var InstallFiles = [];
|
|
1127
|
+
|
|
1128
|
+
// Files to download on full install (currently unused)
|
|
1129
|
+
//var FullInstallFiles = [];
|
|
1130
|
+
|
|
1131
|
+
// Don't delete these files on lump unload
|
|
1132
|
+
var PersistentLumpFiles = [];
|
|
1133
|
+
|
|
1134
|
+
// Files to place in FS on game startup
|
|
1135
|
+
var StartupFiles = [];
|
|
1136
|
+
|
|
1137
|
+
// Files that must be downloaded before game startup
|
|
1138
|
+
var RequiredFiles = [];
|
|
1139
|
+
|
|
1140
|
+
// Store of version bases
|
|
1141
|
+
var VersionBases = {};
|
|
1142
|
+
|
|
1143
|
+
var CompositeKey = (fn, version) => `data/${version}/${fn}`;
|
|
1144
|
+
|
|
1145
|
+
var StripLeadingSeparators = (fn) => fn.replace(/^\/*/, '');
|
|
1146
|
+
|
|
1147
|
+
var ResetProgramData = async (manageLoop = true, callback = null) => {
|
|
1148
|
+
// Delete specific files in IndexedDB /home/web_user -> FILE_DATA
|
|
1149
|
+
// and delete everything in EM_PRELOAD_CACHE -> METADATA, PACKAGES
|
|
1150
|
+
|
|
1151
|
+
let customStore = new idbKeyval.Store('/home/web_user', 'FILE_DATA', 21);
|
|
1152
|
+
let customStoreData = new idbKeyval.Store('SRB2_DATA', 'FILES', 1);
|
|
1153
|
+
let customStoreEmMetadata = new idbKeyval.Store('EM_PRELOAD_CACHE', 'METADATA', 1);
|
|
1154
|
+
let customStoreEmPackages = new idbKeyval.Store('EM_PRELOAD_CACHE', 'PACKAGES', 1);
|
|
1155
|
+
|
|
1156
|
+
if (manageLoop && StartedMainLoop)
|
|
1157
|
+
PauseLoop();
|
|
1158
|
+
|
|
1159
|
+
// Let these fail silently in case the keys do not exist.
|
|
1160
|
+
var silentCatch = _ => {return;};
|
|
1161
|
+
|
|
1162
|
+
await idbKeyval.clear(customStoreEmMetadata).catch(silentCatch);
|
|
1163
|
+
await idbKeyval.clear(customStoreEmPackages).catch(silentCatch);
|
|
1164
|
+
// TODO reliable recursive way to clear multiple versions of data?
|
|
1165
|
+
// because later versions may use older versions as a BASE
|
|
1166
|
+
await idbKeyval.clear(customStoreData).catch(silentCatch);
|
|
1167
|
+
// We don't store game assets in the user folder
|
|
1168
|
+
await idbKeyval.del('/home/web_user/.srb2/music.dta', customStore).catch(silentCatch);
|
|
1169
|
+
|
|
1170
|
+
if (typeof callback === 'function')
|
|
1171
|
+
callback();
|
|
1172
|
+
if (manageLoop && StartedMainLoop)
|
|
1173
|
+
ResumeLoop();
|
|
1174
|
+
|
|
1175
|
+
return Promise.resolve();
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
var InstallProgramFileReferences = async (name, version, fullBase) => {
|
|
1179
|
+
let customStore = new idbKeyval.Store('SRB2_DATA', 'FILES', 1);
|
|
1180
|
+
// Slice all parent bases of the current version, so we don't
|
|
1181
|
+
// overwrite parents' files.
|
|
1182
|
+
let fullBase2 = fullBase.slice(0, fullBase.indexOf(version));
|
|
1183
|
+
|
|
1184
|
+
return Promise.all(fullBase2.map(async base => {
|
|
1185
|
+
if (base && base !== version) {
|
|
1186
|
+
let baseFile = await idbKeyval.get(CompositeKey(name, base), customStore);
|
|
1187
|
+
if (!(baseFile instanceof Object))
|
|
1188
|
+
baseFile = {};
|
|
1189
|
+
if (!('contents' in baseFile)) {
|
|
1190
|
+
if (!('base' in baseFile && baseFile['base'] != version)) {
|
|
1191
|
+
baseFile['base'] = version;
|
|
1192
|
+
console.log(`InstallProgramFileReferences: Logging base ${version} for ${name} (${base})`);
|
|
1193
|
+
return idbKeyval.set(CompositeKey(name, base), baseFile, customStore);
|
|
1194
|
+
} else
|
|
1195
|
+
return Promise.resolve(); // nothing to update
|
|
1196
|
+
}
|
|
1197
|
+
// IF AN ENTRY EXISTS IN A PARENT BASE: Be wary. Don't log anything,
|
|
1198
|
+
// in case the parent file was in error.
|
|
1199
|
+
return Promise.reject('While logging parent base references, parent ${base} already has a file.');
|
|
1200
|
+
}
|
|
1201
|
+
}));
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
var InstallProgramFile = async (name, version, md5 = null, fullBase = []) => {
|
|
1205
|
+
let customStore = new idbKeyval.Store('SRB2_DATA', 'FILES', 1);
|
|
1206
|
+
|
|
1207
|
+
try {
|
|
1208
|
+
let remoteFile = await fetch(`data/${version}/${name}`);
|
|
1209
|
+
|
|
1210
|
+
if (!remoteFile.ok)
|
|
1211
|
+
throw remoteFile;
|
|
1212
|
+
|
|
1213
|
+
remoteFile = await remoteFile.arrayBuffer();
|
|
1214
|
+
remoteFile = new Uint8Array(remoteFile);
|
|
1215
|
+
|
|
1216
|
+
// Log a reference to this version in all the parent bases
|
|
1217
|
+
await InstallProgramFileReferences(name, version, fullBase);
|
|
1218
|
+
|
|
1219
|
+
// Log to persistent storage
|
|
1220
|
+
let idbFile = await idbKeyval.get(CompositeKey(name, version), customStore);
|
|
1221
|
+
if (!idbFile)
|
|
1222
|
+
idbFile = {};
|
|
1223
|
+
idbFile['contents'] = remoteFile;
|
|
1224
|
+
idbFile['md5'] = md5;
|
|
1225
|
+
await idbKeyval.set(CompositeKey(name, version), idbFile, customStore);
|
|
1226
|
+
console.log(`Downloaded ${name} (${version}) from server.`);
|
|
1227
|
+
return Promise.resolve(idbFile);
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
// fetch connection error or other throw from above
|
|
1230
|
+
throw `CheckInstallProgramFile: data/${version}/${name} - ${e}`;
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
var CheckInstallProgramFile = async (name, version, base = [], fullBase = [], checkMd5IfFileExists = false) => {
|
|
1235
|
+
// Every data file on the server has a corresponding *.md5 file.
|
|
1236
|
+
// Check the server for a file in the given version.
|
|
1237
|
+
// If it doesn't exist in the given version, then try the "base" version.
|
|
1238
|
+
|
|
1239
|
+
if (typeof version === 'undefined' || !version) {
|
|
1240
|
+
if (base && base.length)
|
|
1241
|
+
return CheckInstallProgramFile(name, base.shift(), base, fullBase, checkMd5IfFileExists);
|
|
1242
|
+
else
|
|
1243
|
+
throw `CheckInstallProgramFile: ${name} - No version to fetch from!`;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
let customStore = new idbKeyval.Store('SRB2_DATA', 'FILES', 1);
|
|
1247
|
+
|
|
1248
|
+
// First, see if we can get a base reference in IDB
|
|
1249
|
+
try {
|
|
1250
|
+
let localFile = await idbKeyval.get(CompositeKey(name, version), customStore);
|
|
1251
|
+
if (localFile && localFile instanceof Object) {
|
|
1252
|
+
if ('base' in localFile)
|
|
1253
|
+
return await CheckInstallProgramFile(name, localFile['base'], [], fullBase, checkMd5IfFileExists);
|
|
1254
|
+
else if (!checkMd5IfFileExists) {
|
|
1255
|
+
console.log(`Retrieved ${name} (${version}) from storage.`);
|
|
1256
|
+
// Don't log a base reference (InstallProgramFileReferences),
|
|
1257
|
+
// because we want to verify with server before doing so.
|
|
1258
|
+
return Promise.resolve(localFile);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
} catch (e) { } // fail silently; oh well, we tried.
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
// Check server's MD5 file for the current version.
|
|
1265
|
+
let remoteMd5 = await fetch(`data/${version}/${name}.md5`);
|
|
1266
|
+
if (!remoteMd5.ok) {
|
|
1267
|
+
// Server's MD5 was not found: check the "base" version for the file.
|
|
1268
|
+
if (base && base.length && base[0])
|
|
1269
|
+
return await CheckInstallProgramFile(name, base.shift(), base, fullBase, checkMd5IfFileExists);
|
|
1270
|
+
else if (!RequiredFiles.includes(name))
|
|
1271
|
+
return Promise.resolve(); // shrug, we don't need this file.
|
|
1272
|
+
else
|
|
1273
|
+
throw 'Cannot retrieve MD5';
|
|
1274
|
+
} else {
|
|
1275
|
+
// Server's MD5 was found: compare it to our local MD5 for the file.
|
|
1276
|
+
let idbFile = await idbKeyval.get(CompositeKey(name, version), customStore);
|
|
1277
|
+
remoteMd5 = await remoteMd5.text();
|
|
1278
|
+
|
|
1279
|
+
if (idbFile && 'md5' in idbFile
|
|
1280
|
+
&& remoteMd5 && remoteMd5 == idbFile['md5']) {
|
|
1281
|
+
// The MD5's match: we're done, don't download anything.
|
|
1282
|
+
// Log a reference to this version in all the parent bases
|
|
1283
|
+
await InstallProgramFileReferences(name, version, fullBase);
|
|
1284
|
+
console.log(`Retrieved ${name} (${version}) from storage.`);
|
|
1285
|
+
return Promise.resolve(idbFile);
|
|
1286
|
+
}
|
|
1287
|
+
else if (remoteMd5 && remoteMd5.length == 32) { // sanity check
|
|
1288
|
+
// The MD5's do not match: store the server's MD5, and download
|
|
1289
|
+
// the file for the current version.
|
|
1290
|
+
return await InstallProgramFile(name, version, remoteMd5, fullBase);
|
|
1291
|
+
} else {
|
|
1292
|
+
// Edge case where remote MD5 is not valid: see if there's an older file.
|
|
1293
|
+
if (base && base.length && base[0])
|
|
1294
|
+
return await CheckInstallProgramFile(name, base.shift(), base, fullBase, checkMd5IfFileExists);
|
|
1295
|
+
else if (!RequiredFiles.includes(name))
|
|
1296
|
+
return Promise.resolve(); // shrug, we don't need this file.
|
|
1297
|
+
else
|
|
1298
|
+
throw 'Cannot retrieve remote MD5.';
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
// fetch() connection error, or thrown from above code.
|
|
1303
|
+
throw `CheckInstallProgramFile: data/${version}/${name}.md5 - ${e}`;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
var CheckInstallFileList = async (fileList, version = '2.2.4', checkServer = false, progressCallback = null, finishedCallback = null) => {
|
|
1308
|
+
let bases = await GetVersionBases(version);
|
|
1309
|
+
let files = [...fileList];
|
|
1310
|
+
// Using es6-promise-pool to rate-limit file requests.
|
|
1311
|
+
// This works like a loop: when the Pool requests a promise,
|
|
1312
|
+
// it produces promises based on the count index.
|
|
1313
|
+
let count = 0;
|
|
1314
|
+
let promiseProducer = () => {
|
|
1315
|
+
if (count < files.length) {
|
|
1316
|
+
let basesRecursive = [...bases]; // first entry is our own version
|
|
1317
|
+
return CheckInstallProgramFile(files[count++], basesRecursive.shift(), basesRecursive, [...bases], checkServer)
|
|
1318
|
+
.then(_ => {
|
|
1319
|
+
if (typeof progressCallback === 'function')
|
|
1320
|
+
progressCallback(files[count], count, files.length);
|
|
1321
|
+
});
|
|
1322
|
+
} else
|
|
1323
|
+
return null;
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
// Prime the progress callback with the first file
|
|
1327
|
+
if (typeof progressCallback === 'function')
|
|
1328
|
+
progressCallback(files[count], count, files.length);
|
|
1329
|
+
|
|
1330
|
+
let pool = new PromisePool(promiseProducer, 3);
|
|
1331
|
+
|
|
1332
|
+
return pool.start()
|
|
1333
|
+
.then(_ => {
|
|
1334
|
+
if (typeof finishedCallback === 'function')
|
|
1335
|
+
finishedCallback();
|
|
1336
|
+
})
|
|
1337
|
+
.catch(e => { console.error(`CheckInstallFileList: ${e}`); throw e; });
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
var GetVersionFileLists = async (version = '2.2.4') => {
|
|
1341
|
+
let fileLists = {'INSTALL': InstallFiles,
|
|
1342
|
+
//'_FULLINSTALL': FullInstallFiles,
|
|
1343
|
+
'PERSISTENT': PersistentLumpFiles,
|
|
1344
|
+
'STARTUP': StartupFiles,
|
|
1345
|
+
'REQUIRED': RequiredFiles};
|
|
1346
|
+
// Download all file lists
|
|
1347
|
+
try {
|
|
1348
|
+
await CheckInstallFileList(Object.keys(fileLists), version, true, null, null);
|
|
1349
|
+
} catch (e) { console.error('GetVersionFileLists error: ^^^^^'); }
|
|
1350
|
+
|
|
1351
|
+
// Populate local file lists
|
|
1352
|
+
// This is inefficent, but pull the files we just downloaded
|
|
1353
|
+
// Hopefully we got them all, so we just retrieve them from the IDB.
|
|
1354
|
+
await Promise.all(Object.keys(fileLists).map(async (name) => {
|
|
1355
|
+
try {
|
|
1356
|
+
fileLists[name].length = 0; // clear array, keep reference
|
|
1357
|
+
let file = await RetrieveInstalledFile(name, version, false);
|
|
1358
|
+
if (file instanceof Object && 'contents' in file && file.contents) {
|
|
1359
|
+
// Convert to text file
|
|
1360
|
+
let textList = new TextDecoder("utf-8").decode(file.contents);
|
|
1361
|
+
textList = textList.replace('\r\n','\n');
|
|
1362
|
+
textLines = textList.split('\n');
|
|
1363
|
+
// Add entry to file list
|
|
1364
|
+
textLines.forEach(line => {
|
|
1365
|
+
let lineTrim = line.trim();
|
|
1366
|
+
if (lineTrim && !lineTrim.startsWith('//'))
|
|
1367
|
+
fileLists[name].push(lineTrim);
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
} catch (e) {
|
|
1371
|
+
console.error(`GetVersionFileLists: Could not populate list ${name}`, e);
|
|
1372
|
+
}
|
|
1373
|
+
return name;
|
|
1374
|
+
}));
|
|
1375
|
+
|
|
1376
|
+
// Add PersistentLumpFiles to StartupFiles
|
|
1377
|
+
StartupFiles.push(...PersistentLumpFiles);
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
var GetVersionBases = async (version = '2.2.4') => {
|
|
1381
|
+
// Build BASE dependency list
|
|
1382
|
+
// TODO Do MD5 checks on the remote BASE files, in the
|
|
1383
|
+
// rare event that the base version should change.
|
|
1384
|
+
if (VersionBases instanceof Object && version in VersionBases)
|
|
1385
|
+
return VersionBases[version];
|
|
1386
|
+
let base = [version]; // initialize with our own version, so we can shift() from step 1.
|
|
1387
|
+
let baseVer = version;
|
|
1388
|
+
try {
|
|
1389
|
+
// On a brand-new setup, the store SHOULD be first created here.
|
|
1390
|
+
let customStore = new idbKeyval.Store('SRB2_DATA', 'FILES', 1);
|
|
1391
|
+
do {
|
|
1392
|
+
// Check IDB first
|
|
1393
|
+
childBaseVer = await idbKeyval.get(CompositeKey('BASE', baseVer), customStore);
|
|
1394
|
+
if (childBaseVer) {
|
|
1395
|
+
if (!base.includes(childBaseVer)) {
|
|
1396
|
+
base.push(childBaseVer);
|
|
1397
|
+
baseVer = childBaseVer;
|
|
1398
|
+
} else
|
|
1399
|
+
break; // no circular references, thanks
|
|
1400
|
+
} else {
|
|
1401
|
+
// If IDB doesn't have the BASE, then check the server
|
|
1402
|
+
childBaseVer = await fetch(`data/${baseVer}/BASE`);
|
|
1403
|
+
if (!childBaseVer.ok)
|
|
1404
|
+
// assume no base version, but don't store anything
|
|
1405
|
+
// in case this was an error.
|
|
1406
|
+
break;
|
|
1407
|
+
else {
|
|
1408
|
+
childBaseVer = await childBaseVer.text();
|
|
1409
|
+
if (childBaseVer) {
|
|
1410
|
+
if (!base.includes(childBaseVer)) {
|
|
1411
|
+
console.log(`GetVersionBases: Logging Base ${childBaseVer} for version ${baseVer}`);
|
|
1412
|
+
await idbKeyval.set(CompositeKey('BASE', baseVer), childBaseVer, customStore);
|
|
1413
|
+
base.push(childBaseVer);
|
|
1414
|
+
baseVer = childBaseVer;
|
|
1415
|
+
} else {
|
|
1416
|
+
await idbKeyval.set(CompositeKey('BASE', baseVer), '', customStore);
|
|
1417
|
+
break; // no circular references, thanks
|
|
1418
|
+
}
|
|
1419
|
+
} else {
|
|
1420
|
+
await idbKeyval.set(CompositeKey('BASE', baseVer), '', customStore);
|
|
1421
|
+
break; // no further bases
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
} while(baseVer);
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
// connection error, don't log anything or continue
|
|
1428
|
+
console.error(`GetVersionBases: ${version} (baseVer: ${baseVer}) - ${e}`);
|
|
1429
|
+
throw e;
|
|
1430
|
+
}
|
|
1431
|
+
if (!(VersionBases instanceof Object))
|
|
1432
|
+
VersionBases = {};
|
|
1433
|
+
VersionBases[version] = [...base];
|
|
1434
|
+
return base;
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
var InstallProgram = async (full = false, version = '2.2.4', checkServer = false, progressCallback = null, finishedCallback = null) => {
|
|
1438
|
+
let bases = await GetVersionBases(version);
|
|
1439
|
+
await GetVersionFileLists(version);
|
|
1440
|
+
let files = [...InstallFiles]; // if re-implementing FullInstallFiles, do it here and check for `full`
|
|
1441
|
+
return CheckInstallFileList(files, version, checkServer, progressCallback, finishedCallback);
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
// UX Function
|
|
1445
|
+
var UXInstallProgram = (full, version = '2.2.4') => {
|
|
1446
|
+
HideContent();
|
|
1447
|
+
// TODO checkServer -- check for internet connection
|
|
1448
|
+
return InstallProgram(full, version, true, (name, curVal, maxVal) => {
|
|
1449
|
+
// Progress callback
|
|
1450
|
+
let percent = Math.round(curVal/maxVal*100);
|
|
1451
|
+
if (typeof name === 'undefined') {
|
|
1452
|
+
console.log(`InstallProgram: Finished install (${curVal}/${maxVal}`);
|
|
1453
|
+
StatusElement.innerText = `Finishing...`;
|
|
1454
|
+
} else {
|
|
1455
|
+
console.log(`InstallProgram: Retrieving ${name} (${curVal}/${maxVal})`)
|
|
1456
|
+
StatusElement.innerText = `Retrieving ${name.split('/').pop()}...`;
|
|
1457
|
+
}
|
|
1458
|
+
ProgressElement.value = percent;
|
|
1459
|
+
},
|
|
1460
|
+
() => {
|
|
1461
|
+
// Finished callback
|
|
1462
|
+
if (full) {
|
|
1463
|
+
alert('Finished install.');
|
|
1464
|
+
window.location.reload();
|
|
1465
|
+
} else {
|
|
1466
|
+
StatusElement.innerText = 'Loading...';
|
|
1467
|
+
ProgressElement.value = 0;
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
var RetrieveInstalledFile = async (fn, version = '2.2.4', checkServer = false) => {
|
|
1473
|
+
// If IDB file exists, will return the IDB file without checking
|
|
1474
|
+
// the web server.
|
|
1475
|
+
let bases = await GetVersionBases(version);
|
|
1476
|
+
return CheckInstallProgramFile(fn, version, [...bases], [...bases], checkServer);
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
var WriteInstalledFileToFS = async (fn, version = '2.2.4', checkServer = false) => {
|
|
1480
|
+
let idbFile = await RetrieveInstalledFile(fn, version, checkServer);
|
|
1481
|
+
if (idbFile instanceof Object && 'contents' in idbFile && idbFile['contents'])
|
|
1482
|
+
return WriteFS('/', fn, idbFile['contents']);
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
var DeleteInstalledFileFromFS = async (fn, persistentFilenames = []) => {
|
|
1486
|
+
if (!persistentFilenames.includes(fn))
|
|
1487
|
+
return DeleteFS(fn);
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
////////////////////////////////
|
|
1491
|
+
// Addons
|
|
1492
|
+
////////////////////////////////
|
|
1493
|
+
|
|
1494
|
+
var ExtractZipFile = (destBaseDir, buffer) => {
|
|
1495
|
+
return JSZip.loadAsync(buffer)
|
|
1496
|
+
.then((zip) => {
|
|
1497
|
+
let files = [];
|
|
1498
|
+
zip.forEach((path, file) => {
|
|
1499
|
+
if (!file['dir']) {
|
|
1500
|
+
files.push(
|
|
1501
|
+
zip.file(path).async('uint8array')
|
|
1502
|
+
.then((data) => {
|
|
1503
|
+
return WriteFS(destBaseDir, path, data);
|
|
1504
|
+
})
|
|
1505
|
+
);
|
|
1506
|
+
} else {
|
|
1507
|
+
try { FS.mkdir(`${destBaseDir}/${path}`); } catch(e) { }
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
return Promise.all(files)
|
|
1511
|
+
.then(SyncFS)
|
|
1512
|
+
.catch((err) => console.log(err));
|
|
1513
|
+
});
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
var DownloadExtractZipFile = (url, destBaseDir, cors) => {
|
|
1517
|
+
let preUrl = cors ? 'https://cors-anywhere.herokuapp.com/' : '';
|
|
1518
|
+
return fetch(`${preUrl}${url}`, cors ? {mode: 'cors'} : {})
|
|
1519
|
+
.then((response) => response.blob())
|
|
1520
|
+
.then((buffer) => ExtractZipFile(destBaseDir, buffer))
|
|
1521
|
+
.catch((err) => console.log(err));
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
var DownloadDataFile = (url, destBaseDir, path, cors) => {
|
|
1525
|
+
let preUrl = cors ? 'https://cors-anywhere.herokuapp.com/' : '';
|
|
1526
|
+
return fetch(`${preUrl}${url}`, cors ? {mode: 'cors'} : {})
|
|
1527
|
+
.then((response) => response.arrayBuffer())
|
|
1528
|
+
.then((buffer) => WriteFS(destBaseDir, path, new Uint8Array(buffer)))
|
|
1529
|
+
.catch((err) => {
|
|
1530
|
+
console.log(err);
|
|
1531
|
+
});
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
var LoadAddons = () => {
|
|
1535
|
+
let addons = [];
|
|
1536
|
+
let inputs = document.getElementById("addonFilesContainer").querySelectorAll(".addonFilesField:not([id=addonFilesTemplate]) input[type=file]");
|
|
1537
|
+
inputs.forEach((input) => {
|
|
1538
|
+
if (input.files.length) {
|
|
1539
|
+
if (input.files[0].name.endsWith('.zip'))
|
|
1540
|
+
addons.push(ExtractZipFile('/addons', input.files[0]));
|
|
1541
|
+
else{
|
|
1542
|
+
addons.push(input.files[0].arrayBuffer()
|
|
1543
|
+
.then((buffer) => WriteFS('/addons', input.files[0].name.replace(' ', '_'), new Uint8Array(buffer)))
|
|
1544
|
+
.catch((err) => { console.log('LoadAddons():'); console.log(err); })
|
|
1545
|
+
);}
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
return Promise.all(addons)
|
|
1549
|
+
.catch(err => { console.log('LoadAddons() error'); console.log(err); });
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
////////////////////////////////
|
|
1553
|
+
// Emscripten Module -- Runtime Parameters
|
|
1554
|
+
////////////////////////////////
|
|
1555
|
+
|
|
1556
|
+
var SystemArguments = [
|
|
1557
|
+
'+addons_option','CUSTOM',
|
|
1558
|
+
'+addons_folder','/addons',
|
|
1559
|
+
];
|
|
1560
|
+
|
|
1561
|
+
var ControlArguments = [];
|
|
1562
|
+
|
|
1563
|
+
var UserArguments = [];
|
|
1564
|
+
|
|
1565
|
+
var BuildControlArguments = () => {
|
|
1566
|
+
let inputs = ControlsFormElement.querySelectorAll('input, textarea');
|
|
1567
|
+
let files = document.getElementById('addonFilesContainer').querySelectorAll('input[type=file]')
|
|
1568
|
+
let args = [];
|
|
1569
|
+
|
|
1570
|
+
// do files first, so we can use the '-file' operator from SystemArguments
|
|
1571
|
+
let validFile = false;
|
|
1572
|
+
if (document.getElementById('addonStartup').checked) {
|
|
1573
|
+
files.forEach(input => {
|
|
1574
|
+
if (input.files.length && !input.files[0].name.endsWith('.zip')) {
|
|
1575
|
+
if (!validFile) {
|
|
1576
|
+
args.push('-file');
|
|
1577
|
+
validFile = true;
|
|
1578
|
+
}
|
|
1579
|
+
args.push(`/addons/${input.files[0].name.replace(' ', '_')}`);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
inputs.forEach(input => {
|
|
1585
|
+
switch(input.id) {
|
|
1586
|
+
case 'resizeHeight': {
|
|
1587
|
+
let resizeHeight = input.value > 800 ? 0 : input.value;
|
|
1588
|
+
let dims = ResizeDimensions(
|
|
1589
|
+
GetViewportWidth(),
|
|
1590
|
+
GetViewportHeight(),
|
|
1591
|
+
resizeHeight
|
|
1592
|
+
);
|
|
1593
|
+
args.push(
|
|
1594
|
+
'+scr_resizeheight', resizeHeight.toString(),
|
|
1595
|
+
'-width', dims.x.toString(),
|
|
1596
|
+
'-height', dims.y.toString()
|
|
1597
|
+
);
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
case 'playMusic': {
|
|
1601
|
+
let val = (input.checked ? 'on' : 'off');
|
|
1602
|
+
args.push(
|
|
1603
|
+
'+midimusic',val,
|
|
1604
|
+
'+digimusic',val
|
|
1605
|
+
);
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1608
|
+
case 'playSound': {
|
|
1609
|
+
let val = (input.checked ? 'on' : 'off');
|
|
1610
|
+
args.push(
|
|
1611
|
+
'+sounds',val
|
|
1612
|
+
);
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
case 'useMouse': {
|
|
1616
|
+
if (!input.checked)
|
|
1617
|
+
args.push(
|
|
1618
|
+
'-nomouse'
|
|
1619
|
+
);
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
case 'drawDistance': {
|
|
1623
|
+
args.push('+drawdist',DrawDistanceRange[input.value]);
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
case 'shadow': {
|
|
1627
|
+
let val = (input.checked ? 'on' : 'off');
|
|
1628
|
+
args.push(
|
|
1629
|
+
'+shadow',val
|
|
1630
|
+
);
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
case 'midisoundfont': {
|
|
1634
|
+
if (input.checked) {
|
|
1635
|
+
args.push(
|
|
1636
|
+
'+midisoundfont','/florestan.sf2'
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
// Don't trust iPhone to give reliable resize events. Handle ourselves.
|
|
1644
|
+
if (UserAgentIsiPhone())
|
|
1645
|
+
args.push('+scr_resize', 'off');
|
|
1646
|
+
else
|
|
1647
|
+
args.push('+scr_resize', 'on');
|
|
1648
|
+
ControlArguments = args;
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
var BuildUserArguments = () => {
|
|
1652
|
+
let argString = document.getElementById('userArguments').value;
|
|
1653
|
+
let args = [];
|
|
1654
|
+
if (argString.trim().length > 0)
|
|
1655
|
+
args = argString.split(' ');
|
|
1656
|
+
UserArguments = args;
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
var SystemArgumentsToString = () => `${SystemArguments.join(' ')} ${ControlArguments.join(' ')}`;
|
|
1660
|
+
|
|
1661
|
+
////////////////////////////////
|
|
1662
|
+
// Emscripten Module -- Runtime Parameters
|
|
1663
|
+
////////////////////////////////
|
|
1664
|
+
|
|
1665
|
+
var StartedMainLoop = false;
|
|
1666
|
+
var StartedMainLoopCallback = () => {
|
|
1667
|
+
document.getElementById('canvas').style.zIndex = "9999";
|
|
1668
|
+
document.getElementById('canvas').style.opacity = "1.0";
|
|
1669
|
+
document.body.classList.add('startedMainLoop');
|
|
1670
|
+
StartedMainLoop = true;
|
|
1671
|
+
// go scorched earth to save memory
|
|
1672
|
+
DeleteNode(document.getElementById('content'));
|
|
1673
|
+
// unlock mouse by default
|
|
1674
|
+
UnlockMouse(true);
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
var ErrorCrashed = false;
|
|
1678
|
+
|
|
1679
|
+
var ErrorHandler = (e, errorId) => {
|
|
1680
|
+
let index = Module['aliveId'].indexOf(errorId);
|
|
1681
|
+
if (index > -1) {
|
|
1682
|
+
if (!Module['alive'][index] && !ErrorCrashed) {
|
|
1683
|
+
ErrorCrashed = true;
|
|
1684
|
+
let msg = `PROGRAM ERROR\n\n${UserAgentIsMobile() ? "Tap" : "Click"} "${UserAgentIsiOS() ? "Close" : "OK"}" to restart the app.\n\nDEVELOPER INFO`;
|
|
1685
|
+
if (e && e.stack)
|
|
1686
|
+
msg += `\n\n${e.stack}`;
|
|
1687
|
+
else
|
|
1688
|
+
msg += `\n\n${e}`;
|
|
1689
|
+
msg += `\n\nSRB2 Version: ${PackageVersion}\n\nSRB2 Web Version: 1592091069\n\n${navigator.userAgent}`
|
|
1690
|
+
console.error(msg);
|
|
1691
|
+
PauseLoop();
|
|
1692
|
+
try {
|
|
1693
|
+
if ('SDL2' in Module !== 'undefined'
|
|
1694
|
+
&& 'audioContext' in Module['SDL2']
|
|
1695
|
+
&& Module['SDL2'].audioContext)
|
|
1696
|
+
Module['SDL2'].audioContext.close();
|
|
1697
|
+
} catch(e) { }
|
|
1698
|
+
alert(msg);
|
|
1699
|
+
window.location.reload();
|
|
1700
|
+
}
|
|
1701
|
+
Module['alive'].splice(index, 1);
|
|
1702
|
+
Module['aliveId'].splice(index, 1);
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
var InitiateErrorCheck = (e) => {
|
|
1707
|
+
if (!ErrorCrashed && e && typeof e === 'string' && e.includes('exception thrown:')) {
|
|
1708
|
+
let errorId = (+new Date + Math.random().toString(36).slice(-5));
|
|
1709
|
+
Module['alive'].push(false);
|
|
1710
|
+
Module['aliveId'].push(errorId);
|
|
1711
|
+
setTimeout(ErrorHandler, 1000/70, e, errorId); // NEWTICRATE*2
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
var InvalidateErrorChecks = () => {
|
|
1716
|
+
Module['alive'].fill(true);
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
var GetJs = async (version = '2.2.4') => {
|
|
1720
|
+
let customStore = new idbKeyval.Store('SRB2_DATA', 'FILES', 1);
|
|
1721
|
+
let js = await idbKeyval.get(CompositeKey('srb2.js', version), customStore);
|
|
1722
|
+
if (!(js instanceof Object) || !('contents' in js) || !js['contents']) {
|
|
1723
|
+
let msg = 'Runtime Error: Program JS not found!\n\nIf you see this error again, try resetting your program data under Settings > Storage.';
|
|
1724
|
+
console.log(msg);
|
|
1725
|
+
alert(msg);
|
|
1726
|
+
window.location.reload();
|
|
1727
|
+
} else {
|
|
1728
|
+
let string = new TextDecoder('utf-8').decode(js['contents']);
|
|
1729
|
+
return `data:text/javascript;base64,${btoa(string)}`;
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
var GetWasm = async (version = '2.2.4') => {
|
|
1734
|
+
let customStore = new idbKeyval.Store('SRB2_DATA', 'FILES', 1);
|
|
1735
|
+
let wasm = await idbKeyval.get(CompositeKey('srb2.wasm', version), customStore);
|
|
1736
|
+
if (!(wasm instanceof Object) || !('contents' in wasm) || !wasm['contents']) {
|
|
1737
|
+
let msg = 'Runtime Error: Program not found!\n\nIf you see this error again, try resetting your program data under Settings > Storage.';
|
|
1738
|
+
console.log(msg);
|
|
1739
|
+
alert(msg);
|
|
1740
|
+
window.location.reload();
|
|
1741
|
+
} else
|
|
1742
|
+
Module['wasmBinary'] = wasm['contents'];
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
var PackageVersion;
|
|
1746
|
+
|
|
1747
|
+
var Module = {
|
|
1748
|
+
arguments: [],
|
|
1749
|
+
|
|
1750
|
+
calledRun: false,
|
|
1751
|
+
|
|
1752
|
+
alive: [], // part of crash handler
|
|
1753
|
+
|
|
1754
|
+
aliveId: [],
|
|
1755
|
+
|
|
1756
|
+
wasmBinary: null,
|
|
1757
|
+
|
|
1758
|
+
StripLeadingSeparators: StripLeadingSeparators,
|
|
1759
|
+
WriteInstalledFileToFS: WriteInstalledFileToFS,
|
|
1760
|
+
DeleteInstalledFileFromFS: DeleteInstalledFileFromFS,
|
|
1761
|
+
PersistentLumpFiles: PersistentLumpFiles,
|
|
1762
|
+
|
|
1763
|
+
preRun: [() => {
|
|
1764
|
+
BuildControlArguments();
|
|
1765
|
+
BuildUserArguments();
|
|
1766
|
+
Module['arguments'].push(...SystemArguments, ...ControlArguments, ...UserArguments);
|
|
1767
|
+
|
|
1768
|
+
////////////////////////////////
|
|
1769
|
+
// Mount filesystem, then check for music.dta
|
|
1770
|
+
////////////////////////////////
|
|
1771
|
+
|
|
1772
|
+
addRunDependency('mount-filesystem');
|
|
1773
|
+
InitializeFS()
|
|
1774
|
+
.then(()=>{
|
|
1775
|
+
return Promise.all(StartupFiles.map(async fn => {
|
|
1776
|
+
try {
|
|
1777
|
+
await WriteInstalledFileToFS(fn, PackageVersion, false);
|
|
1778
|
+
} catch (e) {
|
|
1779
|
+
let msg = `Runtime Error: File ${fn} not found!\n\nIf you see this error again, try resetting your program data.`;
|
|
1780
|
+
console.error(msg);
|
|
1781
|
+
alert(msg);
|
|
1782
|
+
window.location.reload();
|
|
1783
|
+
return Promise.reject();
|
|
1784
|
+
}
|
|
1785
|
+
}));
|
|
1786
|
+
})
|
|
1787
|
+
.then(LoadAddons)
|
|
1788
|
+
.finally(() => removeRunDependency('mount-filesystem'));
|
|
1789
|
+
}],
|
|
1790
|
+
|
|
1791
|
+
printErr: (e) => {
|
|
1792
|
+
// HACK to catch errors thrown by runIter
|
|
1793
|
+
console.error(e);
|
|
1794
|
+
InitiateErrorCheck(e);
|
|
1795
|
+
},
|
|
1796
|
+
|
|
1797
|
+
quit: (e) => {
|
|
1798
|
+
InitiateErrorCheck(e);
|
|
1799
|
+
},
|
|
1800
|
+
|
|
1801
|
+
onExit: () => {
|
|
1802
|
+
window.location.reload();
|
|
1803
|
+
},
|
|
1804
|
+
|
|
1805
|
+
////////////////////////////////
|
|
1806
|
+
// Platform Stuff
|
|
1807
|
+
////////////////////////////////
|
|
1808
|
+
|
|
1809
|
+
print: (function() {
|
|
1810
|
+
var element = document.getElementById('output');
|
|
1811
|
+
if (element) element.value = ''; // clear browser cache
|
|
1812
|
+
return function(text) {
|
|
1813
|
+
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
|
1814
|
+
if (element) {
|
|
1815
|
+
element.value += text + "\n";
|
|
1816
|
+
element.scrollTop = element.scrollHeight; // focus on bottom
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
})(),
|
|
1820
|
+
// printErr: function(text) {
|
|
1821
|
+
// if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
|
1822
|
+
// console.error(text);
|
|
1823
|
+
// },
|
|
1824
|
+
canvas: (function() {
|
|
1825
|
+
var canvas = document.getElementById('canvas');
|
|
1826
|
+
|
|
1827
|
+
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
|
|
1828
|
+
// application robust, you may want to override this behavior before shipping!
|
|
1829
|
+
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
|
|
1830
|
+
canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
|
|
1831
|
+
|
|
1832
|
+
return canvas;
|
|
1833
|
+
})(),
|
|
1834
|
+
setStatus: function(text) {
|
|
1835
|
+
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
|
1836
|
+
if (text === Module.setStatus.last.text) return;
|
|
1837
|
+
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
|
1838
|
+
var now = Date.now();
|
|
1839
|
+
if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
|
|
1840
|
+
Module.setStatus.last.time = now;
|
|
1841
|
+
Module.setStatus.last.text = text;
|
|
1842
|
+
if (m) {
|
|
1843
|
+
text = m[1];
|
|
1844
|
+
ProgressElement.value = parseInt(m[2])*100;
|
|
1845
|
+
ProgressElement.max = parseInt(m[4])*100;
|
|
1846
|
+
ProgressElement.hidden = false;
|
|
1847
|
+
} else {
|
|
1848
|
+
text = "Starting game... (may take a minute)";
|
|
1849
|
+
}
|
|
1850
|
+
StatusElement.innerHTML = text;
|
|
1851
|
+
},
|
|
1852
|
+
totalDependencies: 0,
|
|
1853
|
+
monitorRunDependencies: function(left) {
|
|
1854
|
+
this.totalDependencies = Math.max(this.totalDependencies, left);
|
|
1855
|
+
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
Module.setStatus('Downloading...');
|
|
1859
|
+
window.onerror = function(event) {
|
|
1860
|
+
// TODO: do not warn on ok events like simulating an infinite loop or exitStatus
|
|
1861
|
+
// Module.setStatus('Exception thrown, see JavaScript console');
|
|
1862
|
+
Module.setStatus = function(text) {
|
|
1863
|
+
if (text) Module.printErr('[post-exception status] ' + text);
|
|
1864
|
+
};
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
////////////////////////////////
|
|
1868
|
+
// iOS Support
|
|
1869
|
+
////////////////////////////////
|
|
1870
|
+
|
|
1871
|
+
// iOS Safari requires us to activate WebAudio context upon user touch.
|
|
1872
|
+
// The runtime will use this same audioContext.
|
|
1873
|
+
// https://paulbakaus.com/tutorials/html5/web-audio-on-ios/
|
|
1874
|
+
var ActivateAudio = function() {
|
|
1875
|
+
if (typeof (Module['SDL2']) === 'undefined') {
|
|
1876
|
+
Module['SDL2'] = {};
|
|
1877
|
+
}
|
|
1878
|
+
var SDL2 = Module['SDL2'];
|
|
1879
|
+
if (typeof(Module['SDL2'].audioContext) !== 'undefined'
|
|
1880
|
+
|| !Module['SDL2'].audioContext) {
|
|
1881
|
+
if (typeof (AudioContext) !== 'undefined') {
|
|
1882
|
+
SDL2.audioContext = new AudioContext();
|
|
1883
|
+
} else if (typeof (webkitAudioContext) !== 'undefined') {
|
|
1884
|
+
SDL2.audioContext = new webkitAudioContext();
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
if (typeof(Module['SDL2'].audioContext) !== 'undefined'
|
|
1888
|
+
&& SDL2.audioContext.currentTime == 0) {
|
|
1889
|
+
var buffer = SDL2.audioContext.createBuffer(1, 1, 44100);
|
|
1890
|
+
var source = SDL2.audioContext.createBufferSource();
|
|
1891
|
+
source.buffer = buffer;
|
|
1892
|
+
source.connect(SDL2.audioContext.destination);
|
|
1893
|
+
source.start();
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
if (Module['calledRun'])
|
|
1897
|
+
Module.ccall('COM_ImmedExecute',
|
|
1898
|
+
null,
|
|
1899
|
+
['string'],
|
|
1900
|
+
['restartaudio']
|
|
1901
|
+
);
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
var HandleViewportChange = () => {
|
|
1905
|
+
ChangeResolution();
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
window.addEventListener('load', function() {
|
|
1909
|
+
if(UserAgentIsiOS()) {
|
|
1910
|
+
if(IsStandalone()) {
|
|
1911
|
+
window.addEventListener('touchend', ActivateAudio, {once: true});
|
|
1912
|
+
window.addEventListener('resize', HandleViewportChange, false);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}, {once: true});
|
|
1916
|
+
|
|
1917
|
+
////////////////////////////////
|
|
1918
|
+
// Pause/Resume
|
|
1919
|
+
////////////////////////////////
|
|
1920
|
+
|
|
1921
|
+
var SuspendAudioContext = () => {
|
|
1922
|
+
if ('SDL2' in Module
|
|
1923
|
+
&& Module['SDL2'] instanceof Object
|
|
1924
|
+
&& 'audioContext' in Module['SDL2']
|
|
1925
|
+
&& Module['SDL2'].audioContext)
|
|
1926
|
+
Module['SDL2'].audioContext.suspend();
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
var ResumeAudioContext = () => {
|
|
1930
|
+
if ('SDL2' in Module
|
|
1931
|
+
&& Module['SDL2'] instanceof Object
|
|
1932
|
+
&& 'audioContext' in Module['SDL2']
|
|
1933
|
+
&& Module['SDL2'].audioContext)
|
|
1934
|
+
Module['SDL2'].audioContext.resume();
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1937
|
+
var PauseLoop = () => {
|
|
1938
|
+
if (Module['calledRun'])
|
|
1939
|
+
Module.ccall('pause_loop',
|
|
1940
|
+
'number',
|
|
1941
|
+
[],
|
|
1942
|
+
[]
|
|
1943
|
+
);
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
var ResumeLoop = () => {
|
|
1947
|
+
if (Module['calledRun'])
|
|
1948
|
+
Module.ccall('resume_loop',
|
|
1949
|
+
'number',
|
|
1950
|
+
[],
|
|
1951
|
+
[]
|
|
1952
|
+
);
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
var DoResume = () => {
|
|
1956
|
+
if (Module['calledRun']) {
|
|
1957
|
+
let SDL2 = null;
|
|
1958
|
+
if (typeof (Module['SDL2']) !== 'undefined')
|
|
1959
|
+
SDL2 = Module['SDL2'];
|
|
1960
|
+
|
|
1961
|
+
if (typeof(SDL2.audioContext) === 'undefined'
|
|
1962
|
+
|| !SDL2
|
|
1963
|
+
|| !(SDL2 instanceof Object)
|
|
1964
|
+
|| !('audioContext' in SDL2)
|
|
1965
|
+
|| !SDL2.audioContext
|
|
1966
|
+
|| SDL2.audioContext.status === 'closed') {
|
|
1967
|
+
SDL2.audioContext = null;
|
|
1968
|
+
if (UserAgentIsiOS())
|
|
1969
|
+
window.addEventListener('touchend', ActivateAudio, {once: true});
|
|
1970
|
+
else
|
|
1971
|
+
ActivateAudio();
|
|
1972
|
+
} else
|
|
1973
|
+
ResumeAudioContext();
|
|
1974
|
+
ResumeLoop();
|
|
1975
|
+
ChangeResolution();
|
|
1976
|
+
}
|
|
1977
|
+
window.removeEventListener('touchend', DoResume, {once: true});
|
|
1978
|
+
window.removeEventListener('click', DoResume, {once: true});
|
|
1979
|
+
window.removeEventListener('keydown', DoResume, {once: true});
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
var HandleVisibilityChange = (e) => {
|
|
1983
|
+
if (Module['calledRun']) {
|
|
1984
|
+
if (e.type === 'focus')
|
|
1985
|
+
DoResume();
|
|
1986
|
+
else {
|
|
1987
|
+
PauseLoop();
|
|
1988
|
+
SuspendAudioContext();
|
|
1989
|
+
// just in case the browser does not fire a 'focus'
|
|
1990
|
+
// event upon our return, allow the user to touch to wake
|
|
1991
|
+
window.addEventListener('touchend', DoResume, {once: true});
|
|
1992
|
+
window.addEventListener('click', DoResume, {once: true});
|
|
1993
|
+
window.addEventListener('keydown', DoResume, {once: true});
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
|
|
1998
|
+
window.addEventListener('load', function() {
|
|
1999
|
+
// on iOS 12, only blur/focus fire on change to home or power button
|
|
2000
|
+
window.addEventListener('blur', HandleVisibilityChange);
|
|
2001
|
+
window.addEventListener('focus', HandleVisibilityChange);
|
|
2002
|
+
}, {once: true});
|
|
2003
|
+
|
|
2004
|
+
////////////////////////////////
|
|
2005
|
+
// On-Screen Keyboard
|
|
2006
|
+
////////////////////////////////
|
|
2007
|
+
|
|
2008
|
+
var KeyCaptureElement = document.getElementById("keyCapture");
|
|
2009
|
+
var TextCaptureElement = document.getElementById("textCapture");
|
|
2010
|
+
var ActiveCaptureElement = null;
|
|
2011
|
+
var CloseDelay = 0;
|
|
2012
|
+
|
|
2013
|
+
var I_RaiseScreenKeyboard = () => {
|
|
2014
|
+
CloseDelay = 0;
|
|
2015
|
+
// iOS users need to touch the keyCapture element to show the
|
|
2016
|
+
// keyboard, so don't close it on the next touch.
|
|
2017
|
+
if (UserAgentIsiOS())
|
|
2018
|
+
CloseDelay++;
|
|
2019
|
+
if (UserAgentIsAndroid()) {
|
|
2020
|
+
ActiveCaptureElement = TextCaptureElement;
|
|
2021
|
+
TextCaptureElement.parentElement.style.zIndex = '100000';
|
|
2022
|
+
TextCaptureElement.parentElement.style.opacity = '1.0';
|
|
2023
|
+
TextCaptureElement.disabled = false;
|
|
2024
|
+
TextCaptureElement.addEventListener('keydown', HandleKey, true);
|
|
2025
|
+
TextCaptureElement.addEventListener('keyup', HandleKey, true);
|
|
2026
|
+
TextCaptureElement.addEventListener('keypress', HandleKey, true);
|
|
2027
|
+
} else
|
|
2028
|
+
ActiveCaptureElement = KeyCaptureElement;
|
|
2029
|
+
KeyCaptureElement.value = ".";
|
|
2030
|
+
KeyCaptureElement.style.zIndex = '99999';
|
|
2031
|
+
KeyCaptureElement.addEventListener('touchend', HandleTouchKeyboard, false);
|
|
2032
|
+
ActiveCaptureElement.focus();
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
var I_KeyboardOnScreen = () => {
|
|
2036
|
+
return document.activeElement === ActiveCaptureElement;
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
var I_CloseScreenKeyboard = () => {
|
|
2040
|
+
ActiveCaptureElement.blur();
|
|
2041
|
+
KeyCaptureElement.style.zIndex = '-99999';
|
|
2042
|
+
TextCaptureElement.parentElement.style.zIndex = '-99999';
|
|
2043
|
+
TextCaptureElement.parentElement.style.opacity = '0.0';
|
|
2044
|
+
TextCaptureElement.disabled = true;
|
|
2045
|
+
KeyCaptureElement.removeEventListener('touchend', HandleTouchKeyboard, false);
|
|
2046
|
+
if (ActiveCaptureElement === TextCaptureElement) {
|
|
2047
|
+
TextCaptureElement.removeEventListener('keydown', CaptureKeyEvent, true);
|
|
2048
|
+
TextCaptureElement.removeEventListener('keyup', CaptureKeyEvent, true);
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
|
|
2052
|
+
var HandleTouchKeyboard = () => {
|
|
2053
|
+
if (CloseDelay-- <= 0)
|
|
2054
|
+
I_CloseScreenKeyboard();
|
|
2055
|
+
};
|
|
2056
|
+
|
|
2057
|
+
var HandleKey = (e) => {
|
|
2058
|
+
// Android: Don't let any key events pass to the program
|
|
2059
|
+
e.stopPropagation();
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
var InjectText = () => {
|
|
2063
|
+
let text = ActiveCaptureElement.value;
|
|
2064
|
+
ActiveCaptureElement.focus();
|
|
2065
|
+
if (Module['calledRun'] && text) {
|
|
2066
|
+
Module.ccall('inject_text',
|
|
2067
|
+
null,
|
|
2068
|
+
['string'],
|
|
2069
|
+
[text]
|
|
2070
|
+
);
|
|
2071
|
+
// ENTER keydown
|
|
2072
|
+
Module.ccall('inject_keycode',
|
|
2073
|
+
null,
|
|
2074
|
+
['int','int'],
|
|
2075
|
+
[13,false]
|
|
2076
|
+
);
|
|
2077
|
+
// ENTER keyup
|
|
2078
|
+
Module.ccall('inject_keycode',
|
|
2079
|
+
null,
|
|
2080
|
+
['int','int'],
|
|
2081
|
+
[13,true]
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
ActiveCaptureElement.value = '';
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
////////////////////////////////
|
|
2088
|
+
// Viewport Changes
|
|
2089
|
+
////////////////////////////////
|
|
2090
|
+
|
|
2091
|
+
var ChangeResolution = (x, y) => {
|
|
2092
|
+
if (Module['calledRun']) {
|
|
2093
|
+
if (typeof x === 'undefined')
|
|
2094
|
+
x = GetViewportWidth();
|
|
2095
|
+
if (typeof y === 'undefined')
|
|
2096
|
+
y = GetViewportHeight();
|
|
2097
|
+
Module.ccall('change_resolution',
|
|
2098
|
+
'number',
|
|
2099
|
+
['number', 'number'],
|
|
2100
|
+
[x, y]
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
var AllowWindowResize = () => {
|
|
2106
|
+
return (!I_KeyboardOnScreen() || !UserAgentIsMobile() ||
|
|
2107
|
+
// If OSK and mobile, be careful about resizing.
|
|
2108
|
+
// We generally want to allow it. Portrait is safe because OSK's are not tall.
|
|
2109
|
+
//
|
|
2110
|
+
// However, landscape is dangerous because the keyboard may take most of the
|
|
2111
|
+
// screen, resulting in a viewport ratio that's way too wide,
|
|
2112
|
+
// resulting in a big window size that can crash the game. So do a sanity check.
|
|
2113
|
+
//
|
|
2114
|
+
// iPhone X = 2.167 ratio
|
|
2115
|
+
GetViewportHeight() > GetViewportWidth() ||
|
|
2116
|
+
GetViewportWidth() / GetViewportHeight() <= 2.2);
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
var LockMouse = () => {
|
|
2120
|
+
if (StartedMainLoop)
|
|
2121
|
+
Module.ccall('lock_mouse', null, [], []);
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
var UnlockMouse = (force = false) => {
|
|
2125
|
+
if (StartedMainLoop) {
|
|
2126
|
+
if (force && document.pointerLockElement)
|
|
2127
|
+
document.exitPointerLock(); // this method should fire again, so don't unlock_mouse right now
|
|
2128
|
+
else if (!document.pointerLockElement)
|
|
2129
|
+
Module.ccall('unlock_mouse', null, [], []);
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
var GetViewportWidth = () => {
|
|
2134
|
+
// if (UserAgentIsAndroid()) {
|
|
2135
|
+
// if (document.fullscreenElement) // chrome android weirdness
|
|
2136
|
+
// return screen.width;
|
|
2137
|
+
// else
|
|
2138
|
+
// return window.innerWidth;
|
|
2139
|
+
// } else
|
|
2140
|
+
return document.documentElement.clientWidth;
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
var GetViewportHeight = () => {
|
|
2144
|
+
// if (UserAgentIsAndroid()) {
|
|
2145
|
+
// if (document.fullscreenElement) // chrome android weirdness
|
|
2146
|
+
// return screen.height;
|
|
2147
|
+
// else
|
|
2148
|
+
// return window.innerHeight;
|
|
2149
|
+
// } else
|
|
2150
|
+
return document.documentElement.clientHeight;
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
var CaptureFullscreenKey = (e) => {
|
|
2154
|
+
// Let F11 do fullscreen
|
|
2155
|
+
if (e instanceof KeyboardEvent && e.key === 'F11')
|
|
2156
|
+
e.stopPropagation();
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
window.addEventListener('mousedown', LockMouse, false);
|
|
2160
|
+
document.addEventListener('pointerlockchange', _=>UnlockMouse(), false);
|
|
2161
|
+
window.addEventListener('load', _=>{
|
|
2162
|
+
document.addEventListener('keydown', CaptureFullscreenKey, true);
|
|
2163
|
+
document.addEventListener('keyup', CaptureFullscreenKey, true);
|
|
2164
|
+
document.addEventListener('keypress', CaptureFullscreenKey, true);
|
|
2165
|
+
}, {once:true});
|
|
2166
|
+
</script>
|
|
2167
|
+
<!-- {{{ SCRIPT }}} -->
|
|
2168
|
+
</body>
|
|
2169
|
+
</html>
|