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.
Files changed (312) hide show
  1. package/Apps/30dollarwebsite.html +403 -0
  2. package/Apps/emulatorjs.html +258 -0
  3. package/Apps/ruffle.html +93 -0
  4. package/Apps/soundboard.html +4 -0
  5. package/Games/WAflash/connect4.html +87 -0
  6. package/Games/WAflash/dealornodeal.html +89 -0
  7. package/Games/WAflash/escapingtheprison.html +87 -0
  8. package/Games/WAflash/fancypants2.html +86 -0
  9. package/Games/WAflash/freegear.html +87 -0
  10. package/Games/WAflash/learntoflyidle.html +87 -0
  11. package/Games/WAflash/papascoop.html +88 -0
  12. package/Games/WAflash/papashotdog.html +88 -0
  13. package/Games/WAflash/papaspancake.html +89 -0
  14. package/Games/WAflash/papaswingeria.html +87 -0
  15. package/Games/WAflash/pottyracers3.html +86 -0
  16. package/Games/WAflash/run.html +88 -0
  17. package/Games/WAflash/run2.html +88 -0
  18. package/Games/WAflash/shoppingcarthero2.html +88 -0
  19. package/Games/WAflash/sonicultimate.html +87 -0
  20. package/Games/WAflash/sugarsugar.html +91 -0
  21. package/Games/WAflash/vex.html +91 -0
  22. package/Games/WAflash/worldshardestgame3.html +89 -0
  23. package/Games/emulated/GBA/emeraldversion.html +30 -0
  24. package/Games/emulated/GBA/fireredversion.html +30 -0
  25. package/Games/emulated/GBA/leafgreenversion.html +30 -0
  26. package/Games/emulated/GBA/rubyversion.html +30 -0
  27. package/Games/emulated/GBA/sapphireversion.html +30 -0
  28. package/Games/emulated/N64/mariokart64.html +27 -0
  29. package/Games/emulated/N64/supermario64.html +30 -0
  30. package/Games/emulated/SNES/earthbound.html +31 -0
  31. package/Games/ruffle/MahjongTower.html +40 -0
  32. package/Games/ruffle/QWOP.html +41 -0
  33. package/Games/ruffle/achievementunlocked.html +41 -0
  34. package/Games/ruffle/achievementunlocked2.html +41 -0
  35. package/Games/ruffle/achievementunlocked3.html +41 -0
  36. package/Games/ruffle/ageofdefense.html +41 -0
  37. package/Games/ruffle/ageofwar.html +41 -0
  38. package/Games/ruffle/ageofwar2.html +41 -0
  39. package/Games/ruffle/airship.html +41 -0
  40. package/Games/ruffle/alien.html +41 -0
  41. package/Games/ruffle/angrybirdshalloween.html +40 -0
  42. package/Games/ruffle/appleshooter.html +41 -0
  43. package/Games/ruffle/axisfootballleague.html +40 -0
  44. package/Games/ruffle/badpiggies.html +41 -0
  45. package/Games/ruffle/bejeweledtwist.html +41 -0
  46. package/Games/ruffle/blinding.html +40 -0
  47. package/Games/ruffle/bloons.html +41 -0
  48. package/Games/ruffle/bloonsupermonkey.html +40 -0
  49. package/Games/ruffle/bloxorz.html +40 -0
  50. package/Games/ruffle/bobtherobber.html +41 -0
  51. package/Games/ruffle/bowling.html +40 -0
  52. package/Games/ruffle/breakingthebank.html +40 -0
  53. package/Games/ruffle/btd.html +40 -0
  54. package/Games/ruffle/btd2.html +40 -0
  55. package/Games/ruffle/btd3.html +41 -0
  56. package/Games/ruffle/btd4.html +41 -0
  57. package/Games/ruffle/btd4expansion.html +41 -0
  58. package/Games/ruffle/btd5.html +41 -0
  59. package/Games/ruffle/bubbleshooter.html +41 -0
  60. package/Games/ruffle/burgerrun.html +41 -0
  61. package/Games/ruffle/burritobisonrevenge.html +41 -0
  62. package/Games/ruffle/crimsonroom.html +41 -0
  63. package/Games/ruffle/cubefield.html +40 -0
  64. package/Games/ruffle/curveball.html +41 -0
  65. package/Games/ruffle/deadzed2.html +40 -0
  66. package/Games/ruffle/dinerdash.html +41 -0
  67. package/Games/ruffle/donkeykong.html +40 -0
  68. package/Games/ruffle/douchebagworkout.html +40 -0
  69. package/Games/ruffle/douchebagworkout2.html +40 -0
  70. package/Games/ruffle/ducklife.html +40 -0
  71. package/Games/ruffle/ducklife2.html +40 -0
  72. package/Games/ruffle/ducklife3.html +40 -0
  73. package/Games/ruffle/ducklife4.html +40 -0
  74. package/Games/ruffle/electricman.html +41 -0
  75. package/Games/ruffle/electricman2.html +41 -0
  76. package/Games/ruffle/factoryballs.html +42 -0
  77. package/Games/ruffle/factoryballs2.html +42 -0
  78. package/Games/ruffle/factoryballs3.html +41 -0
  79. package/Games/ruffle/factoryballs4.html +41 -0
  80. package/Games/ruffle/fancypants.html +41 -0
  81. package/Games/ruffle/fancypants3.html +41 -0
  82. package/Games/ruffle/fleeingthecomplex.html +41 -0
  83. package/Games/ruffle/fltron.html +40 -0
  84. package/Games/ruffle/foresttemple.html +40 -0
  85. package/Games/ruffle/frogger.html +40 -0
  86. package/Games/ruffle/frontlinedefense.html +40 -0
  87. package/Games/ruffle/galagaflash.html +41 -0
  88. package/Games/ruffle/gravitee.html +41 -0
  89. package/Games/ruffle/gravitee2.html +41 -0
  90. package/Games/ruffle/growcube.html +41 -0
  91. package/Games/ruffle/growisland.html +41 -0
  92. package/Games/ruffle/growvalley.html +41 -0
  93. package/Games/ruffle/impossiblequiz.html +41 -0
  94. package/Games/ruffle/impossiblequiz2.html +41 -0
  95. package/Games/ruffle/injustice.html +41 -0
  96. package/Games/ruffle/isoball.html +41 -0
  97. package/Games/ruffle/johnnyupgrade.html +41 -0
  98. package/Games/ruffle/jumpingfinn.html +40 -0
  99. package/Games/ruffle/kaboomz.html +40 -0
  100. package/Games/ruffle/leaguebowling.html +41 -0
  101. package/Games/ruffle/learntofly.html +40 -0
  102. package/Games/ruffle/learntofly2.html +40 -0
  103. package/Games/ruffle/learntofly3.html +40 -0
  104. package/Games/ruffle/lemonade.html +40 -0
  105. package/Games/ruffle/madness.html +40 -0
  106. package/Games/ruffle/madnessa.html +40 -0
  107. package/Games/ruffle/mahjonggardens.html +40 -0
  108. package/Games/ruffle/mahjongtitans.html +40 -0
  109. package/Games/ruffle/mariocombat.html +40 -0
  110. package/Games/ruffle/minecrafttowerdefense.html +41 -0
  111. package/Games/ruffle/minecrafttowerdefense2.html +41 -0
  112. package/Games/ruffle/motherload.html +40 -0
  113. package/Games/ruffle/murder.html +41 -0
  114. package/Games/ruffle/myfriendpedro.html +40 -0
  115. package/Games/ruffle/neonrider.html +41 -0
  116. package/Games/ruffle/nyancatlostinspace.html +40 -0
  117. package/Games/ruffle/pacman.html +40 -0
  118. package/Games/ruffle/pacxon.html +41 -0
  119. package/Games/ruffle/pacxondeluxe.html +41 -0
  120. package/Games/ruffle/pandemic2.html +41 -0
  121. package/Games/ruffle/papas sushi.html +40 -0
  122. package/Games/ruffle/papasbake.html +40 -0
  123. package/Games/ruffle/papasburgeria.html +41 -0
  124. package/Games/ruffle/papascheese.html +40 -0
  125. package/Games/ruffle/papascupcake.html +40 -0
  126. package/Games/ruffle/papasdonut.html +40 -0
  127. package/Games/ruffle/papaspasta.html +42 -0
  128. package/Games/ruffle/papaspizza.html +41 -0
  129. package/Games/ruffle/papastaco.html +42 -0
  130. package/Games/ruffle/plantsvszombies.html +41 -0
  131. package/Games/ruffle/poppit.html +42 -0
  132. package/Games/ruffle/portaltheflashversion.html +42 -0
  133. package/Games/ruffle/pottyracers.html +40 -0
  134. package/Games/ruffle/pottyracers2.html +40 -0
  135. package/Games/ruffle/raftwars.html +40 -0
  136. package/Games/ruffle/redball.html +42 -0
  137. package/Games/ruffle/redball2.html +41 -0
  138. package/Games/ruffle/redball3.html +41 -0
  139. package/Games/ruffle/riddleschool.html +41 -0
  140. package/Games/ruffle/riddleschool2.html +41 -0
  141. package/Games/ruffle/riddleschool3.html +41 -0
  142. package/Games/ruffle/riddleschool4.html +41 -0
  143. package/Games/ruffle/riddleschool5.html +41 -0
  144. package/Games/ruffle/riddletransfer.html +41 -0
  145. package/Games/ruffle/riddletransfer2.html +41 -0
  146. package/Games/ruffle/rollercoaster.html +41 -0
  147. package/Games/ruffle/shoppingcarthero.html +41 -0
  148. package/Games/ruffle/spaceiskey.html +41 -0
  149. package/Games/ruffle/spaceiskey2.html +41 -0
  150. package/Games/ruffle/sprinter.html +41 -0
  151. package/Games/ruffle/stealingthediamond.html +41 -0
  152. package/Games/ruffle/stickwar.html +41 -0
  153. package/Games/ruffle/strikeforceheros.html +40 -0
  154. package/Games/ruffle/strikeforceheros2.html +40 -0
  155. package/Games/ruffle/strikeforcekitty.html +41 -0
  156. package/Games/ruffle/sugarsugar2.html +41 -0
  157. package/Games/ruffle/superfighters.html +41 -0
  158. package/Games/ruffle/supermarioflash.html +41 -0
  159. package/Games/ruffle/supermarioflash2.html +41 -0
  160. package/Games/ruffle/supermarioflash3.html +41 -0
  161. package/Games/ruffle/supersmashflash.html +41 -0
  162. package/Games/ruffle/tactical.html +41 -0
  163. package/Games/ruffle/tactical2.html +41 -0
  164. package/Games/ruffle/tacticaloriginal.html +41 -0
  165. package/Games/ruffle/territorywar.html +41 -0
  166. package/Games/ruffle/tetrix2.html +41 -0
  167. package/Games/ruffle/thefightforglorton.html +41 -0
  168. package/Games/ruffle/thinice.html +41 -0
  169. package/Games/ruffle/treasurehunt.html +40 -0
  170. package/Games/ruffle/unfairmario.html +40 -0
  171. package/Games/ruffle/vex2.html +41 -0
  172. package/Games/ruffle/whenpizzaattacks.html +41 -0
  173. package/Games/ruffle/whg.html +40 -0
  174. package/Games/ruffle/whg2.html +40 -0
  175. package/Games/ruffle/whg4.html +40 -0
  176. package/Games/ruffle/yahootennis.html +40 -0
  177. package/Games/ruffle/zombocalypse.html +41 -0
  178. package/Games/singlefile.html +67 -0
  179. package/Games/sonic-games/sonicthehedgehog.html +29 -0
  180. package/Games/sonic-games/sonicthehedgehog2.html +31 -0
  181. package/Games/sonic-games/sonicthehedgehog3.html +23 -0
  182. package/Games/standalone/10minutestilldawn.html +85 -0
  183. package/Games/standalone/1v1lol.html +299 -0
  184. package/Games/standalone/2048.html +129 -0
  185. package/Games/standalone/2048cupcakes.html +135 -0
  186. package/Games/standalone/Angry Birds.html +20 -0
  187. package/Games/standalone/BlockPost.html +36 -0
  188. package/Games/standalone/BuildNow.gg.html +136 -0
  189. package/Games/standalone/Flappy Dunk.html +169 -0
  190. package/Games/standalone/Granny 2.html +202 -0
  191. package/Games/standalone/Time Shooter 2.html +94 -0
  192. package/Games/standalone/adventure.html +128 -0
  193. package/Games/standalone/angrybirdsshowdown.html +24 -0
  194. package/Games/standalone/badicecream.html +163 -0
  195. package/Games/standalone/badicecream2.html +165 -0
  196. package/Games/standalone/badicecream3.html +165 -0
  197. package/Games/standalone/badtime.html +145 -0
  198. package/Games/standalone/baldi.html +26 -0
  199. package/Games/standalone/basketrandom.html +46 -0
  200. package/Games/standalone/bigtower.html +65 -0
  201. package/Games/standalone/bigtower2.html +20 -0
  202. package/Games/standalone/bitlife.html +22 -0
  203. package/Games/standalone/blockblast.html +94 -0
  204. package/Games/standalone/blockthepig.html +142 -0
  205. package/Games/standalone/bowmasters.html +27 -0
  206. package/Games/standalone/boxingrandom.html +0 -0
  207. package/Games/standalone/candycrush.html +67 -0
  208. package/Games/standalone/carssimulator.html +40 -0
  209. package/Games/standalone/caseclicker.html +310 -0
  210. package/Games/standalone/cellmachine.html +24 -0
  211. package/Games/standalone/choppyorc.html +210 -0
  212. package/Games/standalone/circleo.html +99 -0
  213. package/Games/standalone/clusterrush.html +47 -0
  214. package/Games/standalone/colorswitch.html +224 -0
  215. package/Games/standalone/colortunnel.html +24 -0
  216. package/Games/standalone/cookieclicker.html +150 -0
  217. package/Games/standalone/crazycattle3d.html +246 -0
  218. package/Games/standalone/crossyroad.html +23 -0
  219. package/Games/standalone/cyberpunkracing.html +37 -0
  220. package/Games/standalone/dadish.html +46 -0
  221. package/Games/standalone/dadish2.html +72 -0
  222. package/Games/standalone/deathrun3d.html +69 -0
  223. package/Games/standalone/dogeminer.html +996 -0
  224. package/Games/standalone/dragonvsbricks.html +48 -0
  225. package/Games/standalone/driftboss.html +146 -0
  226. package/Games/standalone/drifthunters.html +123 -0
  227. package/Games/standalone/drivemad.html +41 -0
  228. package/Games/standalone/ducklifebattle.html +131 -0
  229. package/Games/standalone/ducklifespace.html +93 -0
  230. package/Games/standalone/earntodie.html +117 -0
  231. package/Games/standalone/economical.html +77 -0
  232. package/Games/standalone/fireandice.html +22 -0
  233. package/Games/standalone/flappybird.html +137 -0
  234. package/Games/standalone/fnaf.html +115 -0
  235. package/Games/standalone/fnaf2.html +34 -0
  236. package/Games/standalone/fnaf3.html +337 -0
  237. package/Games/standalone/fnaf4.html +338 -0
  238. package/Games/standalone/fnaf4halloween.html +34 -0
  239. package/Games/standalone/fridaynightfunkin.html +76 -0
  240. package/Games/standalone/fruitninja.html +99 -0
  241. package/Games/standalone/galaga.html +216 -0
  242. package/Games/standalone/gdlite.html +255 -0
  243. package/Games/standalone/gladihoppers.html +36 -0
  244. package/Games/standalone/googlefeud.html +1163 -0
  245. package/Games/standalone/granny.html +233 -0
  246. package/Games/standalone/grindcraft.html +44 -0
  247. package/Games/standalone/happywheels.html +19 -0
  248. package/Games/standalone/helixjump.html +30 -0
  249. package/Games/standalone/holeio.html +53 -0
  250. package/Games/standalone/idlebreakout.html +95 -0
  251. package/Games/standalone/jetpack.html +21 -0
  252. package/Games/standalone/magictiles3.html +111 -0
  253. package/Games/standalone/monkeymart.html +365 -0
  254. package/Games/standalone/motox3m.html +1 -0
  255. package/Games/standalone/motox3mpoolparty.html +18 -0
  256. package/Games/standalone/motox3mspookyland.html +68 -0
  257. package/Games/standalone/ngon.html +577 -0
  258. package/Games/standalone/omnombounce.html +82 -0
  259. package/Games/standalone/pacmanoriginal.html +30 -0
  260. package/Games/standalone/papasfreezeria.html +348 -0
  261. package/Games/standalone/paperio2.html +54 -0
  262. package/Games/standalone/parkingfury.html +45 -0
  263. package/Games/standalone/polytrack.html +40 -0
  264. package/Games/standalone/pou.html +73 -0
  265. package/Games/standalone/retrobowl.html +119 -0
  266. package/Games/standalone/retrobowlcollege.html +179 -0
  267. package/Games/standalone/rocketsoccer.html +39 -0
  268. package/Games/standalone/run3.html +64 -0
  269. package/Games/standalone/slender.html +40 -0
  270. package/Games/standalone/slope.html +59 -0
  271. package/Games/standalone/snowrider3d.html +27 -0
  272. package/Games/standalone/soccerrandom.html +0 -0
  273. package/Games/standalone/solitaire.html +32 -0
  274. package/Games/standalone/sonicroboblast2.html +2169 -0
  275. package/Games/standalone/spacewaves.html +239 -0
  276. package/Games/standalone/stack.html +639 -0
  277. package/Games/standalone/station141.html +27 -0
  278. package/Games/standalone/stickmangolf.html +22 -0
  279. package/Games/standalone/stickmanhook.html +39 -0
  280. package/Games/standalone/stuntcars3.html +26 -0
  281. package/Games/standalone/subwaysurfers.html +55 -0
  282. package/Games/standalone/superhot.html +35 -0
  283. package/Games/standalone/superliquidsoccer.html +311 -0
  284. package/Games/standalone/templerun2.html +38 -0
  285. package/Games/standalone/timeshooter.html +99 -0
  286. package/Games/standalone/tinyfishing.html +139 -0
  287. package/Games/standalone/tombofthemask.html +14 -0
  288. package/Games/standalone/triviacrack.html +24 -0
  289. package/Games/standalone/tubejumpers.html +20 -0
  290. package/Games/standalone/ultimatecustomnight.html +34 -0
  291. package/Games/standalone/undertheredsky.html +46 -0
  292. package/Games/standalone/vex3.html +42 -0
  293. package/Games/standalone/vex4.html +38 -0
  294. package/Games/standalone/vex5.html +43 -0
  295. package/Games/standalone/vex6.html +56 -0
  296. package/Games/standalone/vex7.html +53 -0
  297. package/Games/standalone/vex8.html +46 -0
  298. package/Games/standalone/wordle.html +27 -0
  299. package/Games/standalone/worldtour.html +95 -0
  300. package/backup.html +2016 -0
  301. package/games.json +2373 -0
  302. package/index.html +2302 -0
  303. package/new logo.png +0 -0
  304. package/package.json +21 -0
  305. package/port.html +0 -0
  306. package/readme.md +1 -0
  307. package/singlefilegames.json +2266 -0
  308. package/sounds/ambience.mp3 +0 -0
  309. package/sounds/click.mp3 +0 -0
  310. package/sounds/close.mp3 +0 -0
  311. package/sounds/hover.mp3 +0 -0
  312. 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&reg; or Sonic Team.
442
+ </p>
443
+ <p style="font-size:xx-small;">
444
+ Sonic the Hedgehog is a trademark of SEGA&reg;<br/>
445
+ The copyrights of "Sonic the Hedgehog" and all associated characters, names, terms, art, and music thereof belong to SEGA&reg;
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>