@lampdotdev/lamp 0.0.0-stage → 0.36.2-nightly.16

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 (470) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +397 -2
  3. package/Resources/Devices/roku.json +410 -0
  4. package/Resources/Devices/xbox-one.json +367 -0
  5. package/Resources/GameKit/NOTICE.md +63 -0
  6. package/Resources/GameKit/apple/LampPage.swift +1813 -0
  7. package/Resources/GameKit/apple/LampPlayer.swift +46 -0
  8. package/Resources/GameKit/apple/LampWebApp.swift +63 -0
  9. package/Resources/GameKit/apple/LampWebGame.swift +87 -0
  10. package/Resources/GameKit/apple/Native.swift +12 -0
  11. package/Resources/GameKit/build.sh +55 -0
  12. package/Resources/GameKit/lamp_game.h +116 -0
  13. package/Resources/GameKit/lamp_host.h +19 -0
  14. package/Resources/GameKit/lamp_kit.cpp +61 -0
  15. package/Resources/GameKit/lamp_sound.cpp +455 -0
  16. package/Resources/GameKit/lamp_xbox.cpp +374 -0
  17. package/Resources/GameKit/libraries/HandmadeMath.h +3940 -0
  18. package/Resources/GameKit/libraries/cgltf.h +7240 -0
  19. package/Resources/GameKit/libraries/dr_mp3.h +5430 -0
  20. package/Resources/GameKit/libraries/dr_wav.h +9209 -0
  21. package/Resources/GameKit/libraries/sokol_debugtext.h +4754 -0
  22. package/Resources/GameKit/libraries/sokol_gfx.h +29558 -0
  23. package/Resources/GameKit/libraries/sokol_gl.h +4538 -0
  24. package/Resources/GameKit/libraries/sokol_log.h +335 -0
  25. package/Resources/GameKit/libraries/sokol_shape.h +1531 -0
  26. package/Resources/GameKit/libraries/stb_image.h +7988 -0
  27. package/Resources/GameKit/libraries/stb_vorbis.c +5584 -0
  28. package/Resources/GameKit/page/lamp-live.js +672 -0
  29. package/Resources/GameKit/scaffold/Game/game.cpp +142 -0
  30. package/Resources/GameKit/scaffold/Game/scene.glsl +49 -0
  31. package/Resources/GameKit/scaffold-web/Web/game.js +31 -0
  32. package/Resources/GameKit/scaffold-web/Web/index.html +13 -0
  33. package/Resources/GameKit/scaffold-web/Web/making.js +33 -0
  34. package/Resources/GameKit/scaffold-web/Web/world.js +33 -0
  35. package/Resources/GameKit/web/lamp-game.js +3042 -0
  36. package/Resources/GameKit/web/lamp-phaser.js +128 -0
  37. package/Resources/WebHost/App.exe +0 -0
  38. package/Resources/WebHost/Microsoft.Web.WebView2.Core.dll +0 -0
  39. package/Resources/WebHost/WebView2-LICENSE.txt +27 -0
  40. package/Resources/WebHost/lamp_controllers.h +1506 -0
  41. package/Resources/WebHost/lamp_web_host.cpp +1173 -0
  42. package/Resources/WebLibraries/earlier.txt +318 -0
  43. package/Resources/WebLibraries/phaser/LICENSE.md +20 -0
  44. package/Resources/WebLibraries/phaser/NOTICE.md +2 -0
  45. package/Resources/WebLibraries/phaser/phaser.esm.min.js +1 -0
  46. package/Resources/WebLibraries/qrcode/LICENSE +21 -0
  47. package/Resources/WebLibraries/qrcode/NOTICE.md +4 -0
  48. package/Resources/WebLibraries/qrcode/qrcode.js +2297 -0
  49. package/Resources/WebLibraries/rapier/LICENSE +201 -0
  50. package/Resources/WebLibraries/rapier/NOTICE.md +3 -0
  51. package/Resources/WebLibraries/rapier/rapier.js +2 -0
  52. package/Resources/WebLibraries/three/LICENSE +21 -0
  53. package/Resources/WebLibraries/three/NOTICE.md +4 -0
  54. package/Resources/WebLibraries/three/addons/animation/AnimationClipCreator.js +168 -0
  55. package/Resources/WebLibraries/three/addons/animation/CCDIKSolver.js +607 -0
  56. package/Resources/WebLibraries/three/addons/controls/ArcballControls.js +3543 -0
  57. package/Resources/WebLibraries/three/addons/controls/DragControls.js +452 -0
  58. package/Resources/WebLibraries/three/addons/controls/FirstPersonControls.js +512 -0
  59. package/Resources/WebLibraries/three/addons/controls/FlyControls.js +384 -0
  60. package/Resources/WebLibraries/three/addons/controls/MapControls.js +116 -0
  61. package/Resources/WebLibraries/three/addons/controls/OrbitControls.js +1985 -0
  62. package/Resources/WebLibraries/three/addons/controls/PointerLockControls.js +264 -0
  63. package/Resources/WebLibraries/three/addons/controls/TrackballControls.js +1106 -0
  64. package/Resources/WebLibraries/three/addons/controls/TransformControls.js +2003 -0
  65. package/Resources/WebLibraries/three/addons/curves/CurveExtras.js +694 -0
  66. package/Resources/WebLibraries/three/addons/curves/NURBSCurve.js +169 -0
  67. package/Resources/WebLibraries/three/addons/curves/NURBSSurface.js +98 -0
  68. package/Resources/WebLibraries/three/addons/curves/NURBSUtils.js +532 -0
  69. package/Resources/WebLibraries/three/addons/curves/NURBSVolume.js +82 -0
  70. package/Resources/WebLibraries/three/addons/effects/AnaglyphEffect.js +274 -0
  71. package/Resources/WebLibraries/three/addons/effects/AsciiEffect.js +323 -0
  72. package/Resources/WebLibraries/three/addons/effects/OutlineEffect.js +489 -0
  73. package/Resources/WebLibraries/three/addons/effects/ParallaxBarrierEffect.js +155 -0
  74. package/Resources/WebLibraries/three/addons/effects/StereoEffect.js +91 -0
  75. package/Resources/WebLibraries/three/addons/environments/ColorEnvironment.js +59 -0
  76. package/Resources/WebLibraries/three/addons/environments/DebugEnvironment.js +102 -0
  77. package/Resources/WebLibraries/three/addons/environments/RoomEnvironment.js +185 -0
  78. package/Resources/WebLibraries/three/addons/geometries/BoxLineGeometry.js +92 -0
  79. package/Resources/WebLibraries/three/addons/geometries/ConvexGeometry.js +72 -0
  80. package/Resources/WebLibraries/three/addons/geometries/DecalGeometry.js +420 -0
  81. package/Resources/WebLibraries/three/addons/geometries/LoftGeometry.js +355 -0
  82. package/Resources/WebLibraries/three/addons/geometries/ParametricFunctions.js +100 -0
  83. package/Resources/WebLibraries/three/addons/geometries/ParametricGeometry.js +172 -0
  84. package/Resources/WebLibraries/three/addons/geometries/RoundedBoxGeometry.js +216 -0
  85. package/Resources/WebLibraries/three/addons/geometries/TeapotGeometry.js +689 -0
  86. package/Resources/WebLibraries/three/addons/geometries/TextGeometry.js +102 -0
  87. package/Resources/WebLibraries/three/addons/helpers/AnimationPathHelper.js +302 -0
  88. package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelper.js +166 -0
  89. package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelperWebGL.js +221 -0
  90. package/Resources/WebLibraries/three/addons/helpers/LightProbeHelper.js +166 -0
  91. package/Resources/WebLibraries/three/addons/helpers/LightProbeHelperGPU.js +102 -0
  92. package/Resources/WebLibraries/three/addons/helpers/OctreeHelper.js +109 -0
  93. package/Resources/WebLibraries/three/addons/helpers/PositionalAudioHelper.js +169 -0
  94. package/Resources/WebLibraries/three/addons/helpers/RapierHelper.js +59 -0
  95. package/Resources/WebLibraries/three/addons/helpers/RectAreaLightHelper.js +118 -0
  96. package/Resources/WebLibraries/three/addons/helpers/TextureHelper.js +265 -0
  97. package/Resources/WebLibraries/three/addons/helpers/TextureHelperGPU.js +214 -0
  98. package/Resources/WebLibraries/three/addons/helpers/VertexNormalsHelper.js +155 -0
  99. package/Resources/WebLibraries/three/addons/helpers/VertexTangentsHelper.js +133 -0
  100. package/Resources/WebLibraries/three/addons/helpers/ViewHelper.js +529 -0
  101. package/Resources/WebLibraries/three/addons/interactive/HTMLMesh.js +617 -0
  102. package/Resources/WebLibraries/three/addons/interactive/InteractiveGroup.js +224 -0
  103. package/Resources/WebLibraries/three/addons/interactive/SelectionBox.js +324 -0
  104. package/Resources/WebLibraries/three/addons/interactive/SelectionHelper.js +150 -0
  105. package/Resources/WebLibraries/three/addons/libs/fflate.module.js +2699 -0
  106. package/Resources/WebLibraries/three/addons/libs/lil-gui.module.min.js +8 -0
  107. package/Resources/WebLibraries/three/addons/libs/meshopt_decoder.module.js +196 -0
  108. package/Resources/WebLibraries/three/addons/libs/potpack.module.js +125 -0
  109. package/Resources/WebLibraries/three/addons/libs/stats.module.js +167 -0
  110. package/Resources/WebLibraries/three/addons/libs/tween.module.js +876 -0
  111. package/Resources/WebLibraries/three/addons/lighting/ClusteredLighting.js +153 -0
  112. package/Resources/WebLibraries/three/addons/lighting/DynamicLighting.js +82 -0
  113. package/Resources/WebLibraries/three/addons/lighting/LightMapper.js +295 -0
  114. package/Resources/WebLibraries/three/addons/lighting/LightProbeGrid.js +923 -0
  115. package/Resources/WebLibraries/three/addons/lighting/LightProbeGridUtils.js +107 -0
  116. package/Resources/WebLibraries/three/addons/lighting/LightProbeGridWebGL.js +846 -0
  117. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIConeTracer.js +243 -0
  118. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIIrradiance.js +43 -0
  119. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGINode.js +617 -0
  120. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGISceneCollector.js +793 -0
  121. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIVolume.js +1440 -0
  122. package/Resources/WebLibraries/three/addons/lights/LightProbeGenerator.js +337 -0
  123. package/Resources/WebLibraries/three/addons/lights/RectAreaLightTexturesLib.js +127 -0
  124. package/Resources/WebLibraries/three/addons/lights/RectAreaLightUniformsLib.js +40 -0
  125. package/Resources/WebLibraries/three/addons/lights/SunLight.js +100 -0
  126. package/Resources/WebLibraries/three/addons/lights/SunLightNode.js +56 -0
  127. package/Resources/WebLibraries/three/addons/lights/SunLightShadow.js +319 -0
  128. package/Resources/WebLibraries/three/addons/lights/SunShadowNode.js +312 -0
  129. package/Resources/WebLibraries/three/addons/lines/Line2.js +56 -0
  130. package/Resources/WebLibraries/three/addons/lines/LineGeometry.js +157 -0
  131. package/Resources/WebLibraries/three/addons/lines/LineMaterial.js +742 -0
  132. package/Resources/WebLibraries/three/addons/lines/LineSegments2.js +434 -0
  133. package/Resources/WebLibraries/three/addons/lines/LineSegmentsGeometry.js +298 -0
  134. package/Resources/WebLibraries/three/addons/lines/Wireframe.js +108 -0
  135. package/Resources/WebLibraries/three/addons/lines/WireframeGeometry2.js +49 -0
  136. package/Resources/WebLibraries/three/addons/lines/webgpu/Line2.js +46 -0
  137. package/Resources/WebLibraries/three/addons/lines/webgpu/LineSegments2.js +419 -0
  138. package/Resources/WebLibraries/three/addons/lines/webgpu/Wireframe.js +86 -0
  139. package/Resources/WebLibraries/three/addons/loaders/EXRLoader.js +3411 -0
  140. package/Resources/WebLibraries/three/addons/loaders/FBXLoader.js +4660 -0
  141. package/Resources/WebLibraries/three/addons/loaders/FontLoader.js +262 -0
  142. package/Resources/WebLibraries/three/addons/loaders/GLTFLoader.js +5006 -0
  143. package/Resources/WebLibraries/three/addons/loaders/HDRLoader.js +463 -0
  144. package/Resources/WebLibraries/three/addons/loaders/MTLLoader.js +601 -0
  145. package/Resources/WebLibraries/three/addons/loaders/OBJLoader.js +961 -0
  146. package/Resources/WebLibraries/three/addons/loaders/PLYLoader.js +992 -0
  147. package/Resources/WebLibraries/three/addons/loaders/RGBELoader.js +18 -0
  148. package/Resources/WebLibraries/three/addons/loaders/STLLoader.js +423 -0
  149. package/Resources/WebLibraries/three/addons/loaders/SVGLoader.js +3297 -0
  150. package/Resources/WebLibraries/three/addons/loaders/TGALoader.js +539 -0
  151. package/Resources/WebLibraries/three/addons/loaders/UltraHDRLoader.js +757 -0
  152. package/Resources/WebLibraries/three/addons/math/Capsule.js +159 -0
  153. package/Resources/WebLibraries/three/addons/math/ColorConverter.js +62 -0
  154. package/Resources/WebLibraries/three/addons/math/ColorSpaces.js +147 -0
  155. package/Resources/WebLibraries/three/addons/math/ConvexHull.js +1695 -0
  156. package/Resources/WebLibraries/three/addons/math/ImprovedNoise.js +88 -0
  157. package/Resources/WebLibraries/three/addons/math/Lut.js +317 -0
  158. package/Resources/WebLibraries/three/addons/math/MeshSurfaceSampler.js +315 -0
  159. package/Resources/WebLibraries/three/addons/math/OBB.js +535 -0
  160. package/Resources/WebLibraries/three/addons/math/Octree.js +825 -0
  161. package/Resources/WebLibraries/three/addons/math/SimplexNoise.js +470 -0
  162. package/Resources/WebLibraries/three/addons/misc/ConvexObjectBreaker.js +539 -0
  163. package/Resources/WebLibraries/three/addons/misc/GPUComputationRenderer.js +508 -0
  164. package/Resources/WebLibraries/three/addons/misc/Gyroscope.js +78 -0
  165. package/Resources/WebLibraries/three/addons/misc/MorphAnimMesh.js +119 -0
  166. package/Resources/WebLibraries/three/addons/misc/MorphBlendMesh.js +425 -0
  167. package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMap.js +370 -0
  168. package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMapGPU.js +322 -0
  169. package/Resources/WebLibraries/three/addons/misc/RollerCoaster.js +661 -0
  170. package/Resources/WebLibraries/three/addons/misc/Sculptor.js +1679 -0
  171. package/Resources/WebLibraries/three/addons/misc/SculptorMesh.js +1504 -0
  172. package/Resources/WebLibraries/three/addons/misc/SculptorTools.js +1180 -0
  173. package/Resources/WebLibraries/three/addons/misc/SculptorUtils.js +345 -0
  174. package/Resources/WebLibraries/three/addons/misc/TileCreasedNormalsPlugin.js +270 -0
  175. package/Resources/WebLibraries/three/addons/misc/TubePainter.js +611 -0
  176. package/Resources/WebLibraries/three/addons/misc/Volume.js +520 -0
  177. package/Resources/WebLibraries/three/addons/misc/VolumeSlice.js +275 -0
  178. package/Resources/WebLibraries/three/addons/modifiers/CurveModifier.js +374 -0
  179. package/Resources/WebLibraries/three/addons/modifiers/CurveModifierGPU.js +256 -0
  180. package/Resources/WebLibraries/three/addons/modifiers/EdgeSplitModifier.js +299 -0
  181. package/Resources/WebLibraries/three/addons/modifiers/TessellateModifier.js +340 -0
  182. package/Resources/WebLibraries/three/addons/objects/GroundedSkybox.js +69 -0
  183. package/Resources/WebLibraries/three/addons/objects/Lensflare.js +489 -0
  184. package/Resources/WebLibraries/three/addons/objects/LensflareMesh.js +376 -0
  185. package/Resources/WebLibraries/three/addons/objects/MarchingCubes.js +1261 -0
  186. package/Resources/WebLibraries/three/addons/objects/Reflector.js +390 -0
  187. package/Resources/WebLibraries/three/addons/objects/ReflectorForSSRPass.js +393 -0
  188. package/Resources/WebLibraries/three/addons/objects/Refractor.js +389 -0
  189. package/Resources/WebLibraries/three/addons/objects/ShadowMesh.js +130 -0
  190. package/Resources/WebLibraries/three/addons/objects/Sky.js +342 -0
  191. package/Resources/WebLibraries/three/addons/objects/SkyMesh.js +403 -0
  192. package/Resources/WebLibraries/three/addons/objects/Water.js +374 -0
  193. package/Resources/WebLibraries/three/addons/objects/Water2.js +403 -0
  194. package/Resources/WebLibraries/three/addons/objects/Water2Mesh.js +199 -0
  195. package/Resources/WebLibraries/three/addons/objects/WaterMesh.js +194 -0
  196. package/Resources/WebLibraries/three/addons/postprocessing/AfterimagePass.js +187 -0
  197. package/Resources/WebLibraries/three/addons/postprocessing/BloomPass.js +274 -0
  198. package/Resources/WebLibraries/three/addons/postprocessing/BokehPass.js +218 -0
  199. package/Resources/WebLibraries/three/addons/postprocessing/ClearPass.js +97 -0
  200. package/Resources/WebLibraries/three/addons/postprocessing/CubeTexturePass.js +146 -0
  201. package/Resources/WebLibraries/three/addons/postprocessing/DotScreenPass.js +114 -0
  202. package/Resources/WebLibraries/three/addons/postprocessing/EffectComposer.js +365 -0
  203. package/Resources/WebLibraries/three/addons/postprocessing/FXAAPass.js +40 -0
  204. package/Resources/WebLibraries/three/addons/postprocessing/FilmPass.js +113 -0
  205. package/Resources/WebLibraries/three/addons/postprocessing/GTAOPass.js +735 -0
  206. package/Resources/WebLibraries/three/addons/postprocessing/GlitchPass.js +177 -0
  207. package/Resources/WebLibraries/three/addons/postprocessing/HalftonePass.js +134 -0
  208. package/Resources/WebLibraries/three/addons/postprocessing/LUTPass.js +138 -0
  209. package/Resources/WebLibraries/three/addons/postprocessing/MaskPass.js +195 -0
  210. package/Resources/WebLibraries/three/addons/postprocessing/OutlinePass.js +776 -0
  211. package/Resources/WebLibraries/three/addons/postprocessing/OutputPass.js +148 -0
  212. package/Resources/WebLibraries/three/addons/postprocessing/Pass.js +191 -0
  213. package/Resources/WebLibraries/three/addons/postprocessing/RenderPass.js +193 -0
  214. package/Resources/WebLibraries/three/addons/postprocessing/RenderPixelatedPass.js +314 -0
  215. package/Resources/WebLibraries/three/addons/postprocessing/RenderTransitionPass.js +267 -0
  216. package/Resources/WebLibraries/three/addons/postprocessing/SAOPass.js +407 -0
  217. package/Resources/WebLibraries/three/addons/postprocessing/SMAAPass.js +230 -0
  218. package/Resources/WebLibraries/three/addons/postprocessing/SSAARenderPass.js +313 -0
  219. package/Resources/WebLibraries/three/addons/postprocessing/SSAOPass.js +527 -0
  220. package/Resources/WebLibraries/three/addons/postprocessing/SSRPass.js +858 -0
  221. package/Resources/WebLibraries/three/addons/postprocessing/SavePass.js +132 -0
  222. package/Resources/WebLibraries/three/addons/postprocessing/ShaderPass.js +135 -0
  223. package/Resources/WebLibraries/three/addons/postprocessing/TAARenderPass.js +243 -0
  224. package/Resources/WebLibraries/three/addons/postprocessing/TexturePass.js +131 -0
  225. package/Resources/WebLibraries/three/addons/postprocessing/UnrealBloomPass.js +541 -0
  226. package/Resources/WebLibraries/three/addons/shaders/ACESFilmicToneMappingShader.js +96 -0
  227. package/Resources/WebLibraries/three/addons/shaders/AfterimageShader.js +63 -0
  228. package/Resources/WebLibraries/three/addons/shaders/BasicShader.js +36 -0
  229. package/Resources/WebLibraries/three/addons/shaders/BleachBypassShader.js +68 -0
  230. package/Resources/WebLibraries/three/addons/shaders/BlendShader.js +56 -0
  231. package/Resources/WebLibraries/three/addons/shaders/BokehShader.js +151 -0
  232. package/Resources/WebLibraries/three/addons/shaders/BokehShader2.js +404 -0
  233. package/Resources/WebLibraries/three/addons/shaders/BrightnessContrastShader.js +62 -0
  234. package/Resources/WebLibraries/three/addons/shaders/ColorCorrectionShader.js +59 -0
  235. package/Resources/WebLibraries/three/addons/shaders/ColorifyShader.js +57 -0
  236. package/Resources/WebLibraries/three/addons/shaders/ConvolutionShader.js +74 -0
  237. package/Resources/WebLibraries/three/addons/shaders/CopyShader.js +52 -0
  238. package/Resources/WebLibraries/three/addons/shaders/DOFMipMapShader.js +63 -0
  239. package/Resources/WebLibraries/three/addons/shaders/DepthLimitedBlurShader.js +183 -0
  240. package/Resources/WebLibraries/three/addons/shaders/DigitalGlitch.js +104 -0
  241. package/Resources/WebLibraries/three/addons/shaders/DotScreenShader.js +75 -0
  242. package/Resources/WebLibraries/three/addons/shaders/ExposureShader.js +51 -0
  243. package/Resources/WebLibraries/three/addons/shaders/FXAAShader.js +298 -0
  244. package/Resources/WebLibraries/three/addons/shaders/FilmShader.js +73 -0
  245. package/Resources/WebLibraries/three/addons/shaders/FocusShader.js +94 -0
  246. package/Resources/WebLibraries/three/addons/shaders/FreiChenShader.js +103 -0
  247. package/Resources/WebLibraries/three/addons/shaders/GTAOShader.js +437 -0
  248. package/Resources/WebLibraries/three/addons/shaders/GammaCorrectionShader.js +52 -0
  249. package/Resources/WebLibraries/three/addons/shaders/HalftoneShader.js +332 -0
  250. package/Resources/WebLibraries/three/addons/shaders/HorizontalBlurShader.js +68 -0
  251. package/Resources/WebLibraries/three/addons/shaders/HorizontalTiltShiftShader.js +70 -0
  252. package/Resources/WebLibraries/three/addons/shaders/HueSaturationShader.js +74 -0
  253. package/Resources/WebLibraries/three/addons/shaders/KaleidoShader.js +65 -0
  254. package/Resources/WebLibraries/three/addons/shaders/LuminosityHighPassShader.js +68 -0
  255. package/Resources/WebLibraries/three/addons/shaders/LuminosityShader.js +54 -0
  256. package/Resources/WebLibraries/three/addons/shaders/MirrorShader.js +62 -0
  257. package/Resources/WebLibraries/three/addons/shaders/NormalMapShader.js +60 -0
  258. package/Resources/WebLibraries/three/addons/shaders/OutputShader.js +103 -0
  259. package/Resources/WebLibraries/three/addons/shaders/PoissonDenoiseShader.js +230 -0
  260. package/Resources/WebLibraries/three/addons/shaders/RGBShiftShader.js +61 -0
  261. package/Resources/WebLibraries/three/addons/shaders/SAOShader.js +201 -0
  262. package/Resources/WebLibraries/three/addons/shaders/SMAAShader.js +489 -0
  263. package/Resources/WebLibraries/three/addons/shaders/SSAOShader.js +331 -0
  264. package/Resources/WebLibraries/three/addons/shaders/SSRShader.js +396 -0
  265. package/Resources/WebLibraries/three/addons/shaders/SepiaShader.js +57 -0
  266. package/Resources/WebLibraries/three/addons/shaders/SobelOperatorShader.js +98 -0
  267. package/Resources/WebLibraries/three/addons/shaders/SubsurfaceScatteringShader.js +95 -0
  268. package/Resources/WebLibraries/three/addons/shaders/TechnicolorShader.js +51 -0
  269. package/Resources/WebLibraries/three/addons/shaders/ToonShader.js +349 -0
  270. package/Resources/WebLibraries/three/addons/shaders/TriangleBlurShader.js +79 -0
  271. package/Resources/WebLibraries/three/addons/shaders/UnpackDepthRGBAShader.js +60 -0
  272. package/Resources/WebLibraries/three/addons/shaders/VelocityShader.js +137 -0
  273. package/Resources/WebLibraries/three/addons/shaders/VerticalBlurShader.js +66 -0
  274. package/Resources/WebLibraries/three/addons/shaders/VerticalTiltShiftShader.js +70 -0
  275. package/Resources/WebLibraries/three/addons/shaders/VignetteShader.js +56 -0
  276. package/Resources/WebLibraries/three/addons/shaders/VolumeShader.js +285 -0
  277. package/Resources/WebLibraries/three/addons/shaders/WaterRefractionShader.js +106 -0
  278. package/Resources/WebLibraries/three/addons/textures/FlakesTexture.js +54 -0
  279. package/Resources/WebLibraries/three/addons/tsl/WebGLNodesHandler.js +607 -0
  280. package/Resources/WebLibraries/three/addons/tsl/display/AfterImageNode.js +256 -0
  281. package/Resources/WebLibraries/three/addons/tsl/display/AnaglyphPassNode.js +550 -0
  282. package/Resources/WebLibraries/three/addons/tsl/display/BilateralBlurNode.js +377 -0
  283. package/Resources/WebLibraries/three/addons/tsl/display/BleachBypass.js +33 -0
  284. package/Resources/WebLibraries/three/addons/tsl/display/BloomNode.js +599 -0
  285. package/Resources/WebLibraries/three/addons/tsl/display/CRT.js +150 -0
  286. package/Resources/WebLibraries/three/addons/tsl/display/ChromaticAberrationNode.js +163 -0
  287. package/Resources/WebLibraries/three/addons/tsl/display/DenoiseNode.js +354 -0
  288. package/Resources/WebLibraries/three/addons/tsl/display/DepthOfFieldNode.js +572 -0
  289. package/Resources/WebLibraries/three/addons/tsl/display/DotScreenNode.js +104 -0
  290. package/Resources/WebLibraries/three/addons/tsl/display/FSR1Node.js +481 -0
  291. package/Resources/WebLibraries/three/addons/tsl/display/FXAANode.js +357 -0
  292. package/Resources/WebLibraries/three/addons/tsl/display/FilmNode.js +101 -0
  293. package/Resources/WebLibraries/three/addons/tsl/display/GTAONode.js +727 -0
  294. package/Resources/WebLibraries/three/addons/tsl/display/GaussianBlurNode.js +412 -0
  295. package/Resources/WebLibraries/three/addons/tsl/display/GodraysNode.js +604 -0
  296. package/Resources/WebLibraries/three/addons/tsl/display/ImportanceSampledEnvironment.js +564 -0
  297. package/Resources/WebLibraries/three/addons/tsl/display/LensflareNode.js +282 -0
  298. package/Resources/WebLibraries/three/addons/tsl/display/Lut3DNode.js +109 -0
  299. package/Resources/WebLibraries/three/addons/tsl/display/MotionBlur.js +33 -0
  300. package/Resources/WebLibraries/three/addons/tsl/display/OITPassNode.js +341 -0
  301. package/Resources/WebLibraries/three/addons/tsl/display/OutlineNode.js +916 -0
  302. package/Resources/WebLibraries/three/addons/tsl/display/ParallaxBarrierPassNode.js +90 -0
  303. package/Resources/WebLibraries/three/addons/tsl/display/PixelationPassNode.js +227 -0
  304. package/Resources/WebLibraries/three/addons/tsl/display/RGBShiftNode.js +96 -0
  305. package/Resources/WebLibraries/three/addons/tsl/display/RecurrentDenoiseNode.js +812 -0
  306. package/Resources/WebLibraries/three/addons/tsl/display/RetroPassNode.js +263 -0
  307. package/Resources/WebLibraries/three/addons/tsl/display/SMAANode.js +729 -0
  308. package/Resources/WebLibraries/three/addons/tsl/display/SSAAPassNode.js +344 -0
  309. package/Resources/WebLibraries/three/addons/tsl/display/SSAONode.js +422 -0
  310. package/Resources/WebLibraries/three/addons/tsl/display/SSGINode.js +698 -0
  311. package/Resources/WebLibraries/three/addons/tsl/display/SSRNode.js +1430 -0
  312. package/Resources/WebLibraries/three/addons/tsl/display/SSSNode.js +479 -0
  313. package/Resources/WebLibraries/three/addons/tsl/display/Sepia.js +24 -0
  314. package/Resources/WebLibraries/three/addons/tsl/display/Shape.js +29 -0
  315. package/Resources/WebLibraries/three/addons/tsl/display/SharpenNode.js +284 -0
  316. package/Resources/WebLibraries/three/addons/tsl/display/SobelOperatorNode.js +168 -0
  317. package/Resources/WebLibraries/three/addons/tsl/display/StereoCompositePassNode.js +190 -0
  318. package/Resources/WebLibraries/three/addons/tsl/display/StereoPassNode.js +117 -0
  319. package/Resources/WebLibraries/three/addons/tsl/display/TAAUNode.js +700 -0
  320. package/Resources/WebLibraries/three/addons/tsl/display/TRAANode.js +609 -0
  321. package/Resources/WebLibraries/three/addons/tsl/display/TemporalReprojectNode.js +1024 -0
  322. package/Resources/WebLibraries/three/addons/tsl/display/TransitionNode.js +141 -0
  323. package/Resources/WebLibraries/three/addons/tsl/display/boxBlur.js +65 -0
  324. package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlend.js +80 -0
  325. package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlur.js +63 -0
  326. package/Resources/WebLibraries/three/addons/tsl/display/hashBlur.js +55 -0
  327. package/Resources/WebLibraries/three/addons/tsl/display/lensDirt.js +30 -0
  328. package/Resources/WebLibraries/three/addons/tsl/display/radialBlur.js +68 -0
  329. package/Resources/WebLibraries/three/addons/tsl/lighting/ClusteredLightsNode.js +994 -0
  330. package/Resources/WebLibraries/three/addons/tsl/lighting/DynamicLightsNode.js +201 -0
  331. package/Resources/WebLibraries/three/addons/tsl/lighting/LightProbeGridNode.js +183 -0
  332. package/Resources/WebLibraries/three/addons/tsl/lighting/data/AmbientLightDataNode.js +67 -0
  333. package/Resources/WebLibraries/three/addons/tsl/lighting/data/DirectionalLightDataNode.js +117 -0
  334. package/Resources/WebLibraries/three/addons/tsl/lighting/data/HemisphereLightDataNode.js +105 -0
  335. package/Resources/WebLibraries/three/addons/tsl/lighting/data/PointLightDataNode.js +140 -0
  336. package/Resources/WebLibraries/three/addons/tsl/lighting/data/SpotLightDataNode.js +167 -0
  337. package/Resources/WebLibraries/three/addons/tsl/math/Bayer.js +84 -0
  338. package/Resources/WebLibraries/three/addons/tsl/math/curlNoise.js +107 -0
  339. package/Resources/WebLibraries/three/addons/tsl/math/voronoiNoise.js +112 -0
  340. package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNode.js +542 -0
  341. package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNodeHelper.js +212 -0
  342. package/Resources/WebLibraries/three/addons/tsl/utils/GroundedSkybox.js +62 -0
  343. package/Resources/WebLibraries/three/addons/tsl/utils/RNoise.js +51 -0
  344. package/Resources/WebLibraries/three/addons/tsl/utils/Raymarching.js +70 -0
  345. package/Resources/WebLibraries/three/addons/tsl/utils/SoftParticles.js +53 -0
  346. package/Resources/WebLibraries/three/addons/tsl/utils/SpecularHelpers.js +302 -0
  347. package/Resources/WebLibraries/three/addons/tsl/utils/TAAUtils.js +184 -0
  348. package/Resources/WebLibraries/three/addons/utils/BufferGeometryUtils.js +1523 -0
  349. package/Resources/WebLibraries/three/addons/utils/CameraUtils.js +82 -0
  350. package/Resources/WebLibraries/three/addons/utils/ColorUtils.js +76 -0
  351. package/Resources/WebLibraries/three/addons/utils/GaussianSplatUtils.js +229 -0
  352. package/Resources/WebLibraries/three/addons/utils/GeometryCompressionUtils.js +547 -0
  353. package/Resources/WebLibraries/three/addons/utils/GeometryUtils.js +226 -0
  354. package/Resources/WebLibraries/three/addons/utils/LDrawUtils.js +211 -0
  355. package/Resources/WebLibraries/three/addons/utils/SceneOptimizer.js +459 -0
  356. package/Resources/WebLibraries/three/addons/utils/SceneUtils.js +363 -0
  357. package/Resources/WebLibraries/three/addons/utils/ShadowMapViewer.js +244 -0
  358. package/Resources/WebLibraries/three/addons/utils/ShadowMapViewerGPU.js +233 -0
  359. package/Resources/WebLibraries/three/addons/utils/SkeletonUtils.js +496 -0
  360. package/Resources/WebLibraries/three/addons/utils/SortUtils.js +175 -0
  361. package/Resources/WebLibraries/three/addons/utils/UVsDebug.js +173 -0
  362. package/Resources/WebLibraries/three/addons/utils/WebGLTextureUtils.js +115 -0
  363. package/Resources/WebLibraries/three/addons/utils/WebGPUTextureUtils.js +81 -0
  364. package/Resources/WebLibraries/three/addons/utils/WorkerPool.js +167 -0
  365. package/Resources/WebLibraries/three/addons/utils/unwrapUVs.js +865 -0
  366. package/Resources/WebLibraries/three/three.core.js +61162 -0
  367. package/Resources/WebLibraries/three/three.module.js +19537 -0
  368. package/Resources/WebLibraries/three/three.tsl.js +41903 -0
  369. package/Resources/WebLibraries/three/three.webgpu.js +53714 -0
  370. package/Resources/share-rules.json +64 -0
  371. package/Resources/skills/2d-game/SKILL.md +305 -0
  372. package/Resources/skills/3d-game/SKILL.md +274 -0
  373. package/Resources/skills/accounts/SKILL.md +41 -0
  374. package/Resources/skills/android-design/SKILL.md +159 -0
  375. package/Resources/skills/app-ai/SKILL.md +154 -0
  376. package/Resources/skills/app-intents/SKILL.md +31 -0
  377. package/Resources/skills/app-kinds/SKILL.md +122 -0
  378. package/Resources/skills/apple-watch/SKILL.md +43 -0
  379. package/Resources/skills/assets/SKILL.md +183 -0
  380. package/Resources/skills/building-blocks/SKILL.md +97 -0
  381. package/Resources/skills/device-control/SKILL.md +56 -0
  382. package/Resources/skills/device-integrations/SKILL.md +278 -0
  383. package/Resources/skills/finder-files/SKILL.md +19 -0
  384. package/Resources/skills/game/SKILL.md +416 -0
  385. package/Resources/skills/game-3d/SKILL.md +12 -0
  386. package/Resources/skills/game-art/SKILL.md +11 -0
  387. package/Resources/skills/game-feel/SKILL.md +13 -0
  388. package/Resources/skills/game-levels/SKILL.md +12 -0
  389. package/Resources/skills/game-live/SKILL.md +12 -0
  390. package/Resources/skills/game-physics/SKILL.md +13 -0
  391. package/Resources/skills/game-playtest/SKILL.md +26 -0
  392. package/Resources/skills/game-ui/SKILL.md +278 -0
  393. package/Resources/skills/github-projects/SKILL.md +62 -0
  394. package/Resources/skills/icloud-sync/SKILL.md +30 -0
  395. package/Resources/skills/iphone-hardware/SKILL.md +57 -0
  396. package/Resources/skills/iphone-widgets/SKILL.md +53 -0
  397. package/Resources/skills/lg-developer-mode/SKILL.md +152 -0
  398. package/Resources/skills/mac-widgets/SKILL.md +60 -0
  399. package/Resources/skills/maps/SKILL.md +8 -0
  400. package/Resources/skills/menu-bar/SKILL.md +36 -0
  401. package/Resources/skills/more-apps/SKILL.md +53 -0
  402. package/Resources/skills/phone-controllers/SKILL.md +79 -0
  403. package/Resources/skills/phone-pages/SKILL.md +173 -0
  404. package/Resources/skills/roku-developer-mode/SKILL.md +106 -0
  405. package/Resources/skills/scene-3d/SKILL.md +27 -0
  406. package/Resources/skills/scripting/SKILL.md +17 -0
  407. package/Resources/skills/share-extension/SKILL.md +22 -0
  408. package/Resources/skills/tv-design/SKILL.md +72 -0
  409. package/Resources/skills/web-app/SKILL.md +170 -0
  410. package/Resources/skills/web-game/SKILL.md +95 -0
  411. package/Resources/skills/web-or-native/SKILL.md +108 -0
  412. package/Resources/skills/wish-links/SKILL.md +156 -0
  413. package/THIRD_PARTY_NOTICES.md +170 -0
  414. package/cli/ai.mjs +237 -0
  415. package/cli/android.mjs +941 -0
  416. package/cli/app-ai.mjs +473 -0
  417. package/cli/browser.mjs +284 -0
  418. package/cli/calls.mjs +594 -0
  419. package/cli/dev.mjs +820 -0
  420. package/cli/devices.mjs +984 -0
  421. package/cli/lamp.mjs +1326 -0
  422. package/cli/lampapp.mjs +332 -0
  423. package/cli/lamps/android-host/LampPageGateway.kt +345 -0
  424. package/cli/lamps/android-host/app/build.gradle.kts +40 -0
  425. package/cli/lamps/android-host/build.gradle.kts +4 -0
  426. package/cli/lamps/android-host/game/AndroidManifest.xml +33 -0
  427. package/cli/lamps/android-host/game/MainActivity.kt +173 -0
  428. package/cli/lamps/android-host/gradle/libs.versions.toml +39 -0
  429. package/cli/lamps/android-host/gradle.properties +8 -0
  430. package/cli/lamps/android-host/page/AndroidManifest.xml +35 -0
  431. package/cli/lamps/android-host/page/MainActivity.kt +193 -0
  432. package/cli/lamps/android-host/settings.gradle.kts +30 -0
  433. package/cli/lamps/android-host/tools.json +43 -0
  434. package/cli/lamps/lamp-controllers.js +2 -0
  435. package/cli/lamps/lamp-for-games.js +6922 -0
  436. package/cli/lamps/lamp.js +7082 -0
  437. package/cli/lamps/mac-starter/.lamp/meta.json +1 -0
  438. package/cli/lamps/mac-starter/APP.md +12 -0
  439. package/cli/lamps/mac-starter/Package.swift +8 -0
  440. package/cli/lamps/mac-starter/Sources/App.swift +11 -0
  441. package/cli/lamps/mac-starter/Sources/ContentView.swift +17 -0
  442. package/cli/lamps/mac-starter/Sources/LampSupport.swift +18215 -0
  443. package/cli/lamps/mac-starter/build.sh +336 -0
  444. package/cli/lamps/mac-starter/gitignore +3 -0
  445. package/cli/lamps/mac-starter/lamp-starter.json +6 -0
  446. package/cli/lamps/web-starter/APP.md +8 -0
  447. package/cli/lamps/web-starter/app.js +1 -0
  448. package/cli/lamps/web-starter/index.html +17 -0
  449. package/cli/lamps/web-starter/lamp-starter.json +5 -0
  450. package/cli/lamps/web-starter/lamp.js +7082 -0
  451. package/cli/lamps/web-starter/style.css +25 -0
  452. package/cli/link.mjs +202 -0
  453. package/cli/local-ai.mjs +193 -0
  454. package/cli/mac.mjs +621 -0
  455. package/cli/mdns.mjs +205 -0
  456. package/cli/msix.mjs +413 -0
  457. package/cli/network.mjs +209 -0
  458. package/cli/openrouter.mjs +133 -0
  459. package/cli/outside.mjs +203 -0
  460. package/cli/page.js +317 -0
  461. package/cli/place.mjs +298 -0
  462. package/cli/png.mjs +184 -0
  463. package/cli/share.mjs +270 -0
  464. package/cli/skill/SKILL.md +280 -0
  465. package/cli/skills.mjs +71 -0
  466. package/cli/state.mjs +176 -0
  467. package/cli/update.mjs +162 -0
  468. package/package.json +20 -4
  469. package/site/lib/quota.js +59 -0
  470. package/site/lib/shared-wishes.js +1727 -0
@@ -0,0 +1,1506 @@
1
+ // Phones as controllers for a game on the Xbox, run by the game's own host (lamp_web_host.cpp) with no Lamp: the phone
2
+ // page from the game's package and the relay its phones and its page meet at, as a Mac game runs them in its own
3
+ // process (LampRelayKit's ControllerRuntime, PhonePageServer and Relay, whose wire this keeps to message for message:
4
+ // ControllerWire, ControllerToken). Only the game part of Lamp's relay is here: a wish's room stays at Lamp's.
5
+ //
6
+ // Plain C++ with nothing of Windows in it, so the same protocol runs on a Mac too (scripts/check-web-host-controllers.sh).
7
+ // Whoever includes it supplies the platform's randomness, HMAC-SHA256 and SHA-1 (`lampc::platform`), and the sockets:
8
+ // a `Transport` under each WebSocket, and the bytes of each page request. Everything here runs on one thread.
9
+ #pragma once
10
+ #include <algorithm>
11
+ #include <array>
12
+ #include <chrono>
13
+ #include <cmath>
14
+ #include <cstdint>
15
+ #include <cstdio>
16
+ #include <cstdlib>
17
+ #include <cstring>
18
+ #include <deque>
19
+ #include <functional>
20
+ #include <map>
21
+ #include <memory>
22
+ #include <optional>
23
+ #include <set>
24
+ #include <string>
25
+ #include <string_view>
26
+ #include <vector>
27
+
28
+ namespace lampc {
29
+
30
+ namespace platform {
31
+ // Cryptographically random bytes from the system.
32
+ void random_bytes(uint8_t* out, size_t count);
33
+ std::array<uint8_t, 32> hmac_sha256(std::string_view key, std::string_view data);
34
+ std::array<uint8_t, 20> sha1(std::string_view data);
35
+ }
36
+
37
+ // ControllerWire's numbers, which lamp.js and lamp-controller.js keep too.
38
+ namespace wire {
39
+ constexpr size_t message = 16 * 1024;
40
+ constexpr size_t chunk = 64 * 1024;
41
+ constexpr size_t header = 8;
42
+ constexpr int64_t window = 512 * 1024;
43
+ constexpr int64_t file = 64 * 1024 * 1024;
44
+ constexpr size_t transfers = 2;
45
+ constexpr int players = 8;
46
+ constexpr double per_second = 120;
47
+ constexpr size_t backlog = 32;
48
+ constexpr int64_t silence_ms = 15000;
49
+ constexpr int64_t beat_ms = 5000;
50
+ // The largest message a socket takes: a file's piece with its header, sent by a browser as one frame.
51
+ constexpr size_t message_limit = chunk + 1024;
52
+ }
53
+
54
+ inline int64_t wall_ms() {
55
+ return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
56
+ }
57
+
58
+ // MARK: JSON
59
+
60
+ // Enough JSON for the controllers' messages: objects keep their keys sorted (a credential's body is written that way),
61
+ // numbers are doubles, and text is UTF-8 passed through as it came.
62
+ struct Json {
63
+ enum class Type { null, boolean, number, string, array, object };
64
+ Type type = Type::null;
65
+ bool b = false;
66
+ double n = 0;
67
+ std::string s;
68
+ std::vector<Json> a;
69
+ std::map<std::string, Json> o;
70
+
71
+ Json() = default;
72
+ Json(bool v) : type(Type::boolean), b(v) {}
73
+ Json(int v) : type(Type::number), n(v) {}
74
+ Json(int64_t v) : type(Type::number), n((double)v) {}
75
+ Json(double v) : type(Type::number), n(v) {}
76
+ Json(const char* v) : type(Type::string), s(v) {}
77
+ Json(std::string v) : type(Type::string), s(std::move(v)) {}
78
+ static Json object() { Json j; j.type = Type::object; return j; }
79
+ static Json array() { Json j; j.type = Type::array; return j; }
80
+
81
+ Json& operator[](const std::string& key) { type = Type::object; return o[key]; }
82
+ const Json* get(const std::string& key) const {
83
+ if (type != Type::object) return nullptr;
84
+ auto found = o.find(key);
85
+ return found == o.end() ? nullptr : &found->second;
86
+ }
87
+ std::optional<std::string> str(const std::string& key) const {
88
+ auto v = get(key);
89
+ if (!v || v->type != Type::string) return std::nullopt;
90
+ return v->s;
91
+ }
92
+ // A whole number, as Swift's `as? Int` reads one.
93
+ std::optional<int64_t> integer(const std::string& key) const {
94
+ auto v = get(key);
95
+ if (!v || v->type != Type::number || !std::isfinite(v->n) || v->n != std::floor(v->n) || std::fabs(v->n) > 9.0e15) return std::nullopt;
96
+ return (int64_t)v->n;
97
+ }
98
+ bool is_true(const std::string& key) const {
99
+ auto v = get(key);
100
+ return v && v->type == Type::boolean && v->b;
101
+ }
102
+
103
+ std::string dump() const {
104
+ std::string out;
105
+ write(out);
106
+ return out;
107
+ }
108
+
109
+ void write(std::string& out) const {
110
+ switch (type) {
111
+ case Type::null: out += "null"; break;
112
+ case Type::boolean: out += b ? "true" : "false"; break;
113
+ case Type::number: {
114
+ char buffer[32];
115
+ if (!std::isfinite(n)) out += "0";
116
+ else if (n == std::floor(n) && std::fabs(n) < 9.0e15) { std::snprintf(buffer, sizeof buffer, "%lld", (long long)n); out += buffer; }
117
+ else { std::snprintf(buffer, sizeof buffer, "%.17g", n); out += buffer; }
118
+ break;
119
+ }
120
+ case Type::string: quote(out, s); break;
121
+ case Type::array:
122
+ out += '[';
123
+ for (size_t i = 0; i < a.size(); i++) { if (i) out += ','; a[i].write(out); }
124
+ out += ']';
125
+ break;
126
+ case Type::object: {
127
+ out += '{';
128
+ bool first = true;
129
+ for (auto const& [key, value] : o) {
130
+ if (!first) out += ',';
131
+ first = false;
132
+ quote(out, key);
133
+ out += ':';
134
+ value.write(out);
135
+ }
136
+ out += '}';
137
+ break;
138
+ }
139
+ }
140
+ }
141
+
142
+ static void quote(std::string& out, std::string_view text) {
143
+ out += '"';
144
+ for (unsigned char c : text) {
145
+ switch (c) {
146
+ case '"': out += "\\\""; break;
147
+ case '\\': out += "\\\\"; break;
148
+ case '\n': out += "\\n"; break;
149
+ case '\r': out += "\\r"; break;
150
+ case '\t': out += "\\t"; break;
151
+ default:
152
+ if (c < 0x20) { char u[8]; std::snprintf(u, sizeof u, "\\u%04x", c); out += u; }
153
+ else out += (char)c;
154
+ }
155
+ }
156
+ out += '"';
157
+ }
158
+
159
+ static std::optional<Json> parse(std::string_view text) {
160
+ Parser p{ text, 0 };
161
+ Json value;
162
+ if (!p.value(value, 0)) return std::nullopt;
163
+ p.space();
164
+ if (p.at != text.size()) return std::nullopt;
165
+ return value;
166
+ }
167
+
168
+ private:
169
+ struct Parser {
170
+ std::string_view t;
171
+ size_t at;
172
+ void space() { while (at < t.size() && (t[at] == ' ' || t[at] == '\t' || t[at] == '\n' || t[at] == '\r')) at++; }
173
+ bool word(std::string_view w) {
174
+ if (t.substr(at, w.size()) != w) return false;
175
+ at += w.size();
176
+ return true;
177
+ }
178
+ bool value(Json& out, int depth) {
179
+ if (depth > 64) return false;
180
+ space();
181
+ if (at >= t.size()) return false;
182
+ char c = t[at];
183
+ if (c == '{') {
184
+ at++;
185
+ out = object();
186
+ space();
187
+ if (at < t.size() && t[at] == '}') { at++; return true; }
188
+ for (;;) {
189
+ space();
190
+ std::string key;
191
+ if (at >= t.size() || t[at] != '"' || !string(key)) return false;
192
+ space();
193
+ if (at >= t.size() || t[at++] != ':') return false;
194
+ Json item;
195
+ if (!value(item, depth + 1)) return false;
196
+ out.o[key] = std::move(item);
197
+ space();
198
+ if (at >= t.size()) return false;
199
+ if (t[at] == ',') { at++; continue; }
200
+ if (t[at] == '}') { at++; return true; }
201
+ return false;
202
+ }
203
+ }
204
+ if (c == '[') {
205
+ at++;
206
+ out = array();
207
+ space();
208
+ if (at < t.size() && t[at] == ']') { at++; return true; }
209
+ for (;;) {
210
+ Json item;
211
+ if (!value(item, depth + 1)) return false;
212
+ out.a.push_back(std::move(item));
213
+ space();
214
+ if (at >= t.size()) return false;
215
+ if (t[at] == ',') { at++; continue; }
216
+ if (t[at] == ']') { at++; return true; }
217
+ return false;
218
+ }
219
+ }
220
+ if (c == '"') { out = Json(std::string()); return string(out.s); }
221
+ if (word("true")) { out = Json(true); return true; }
222
+ if (word("false")) { out = Json(false); return true; }
223
+ if (word("null")) { out = Json(); return true; }
224
+ return number(out);
225
+ }
226
+ bool number(Json& out) {
227
+ size_t start = at;
228
+ if (at < t.size() && t[at] == '-') at++;
229
+ if (at >= t.size() || !isdigit((unsigned char)t[at])) return false;
230
+ while (at < t.size() && (isdigit((unsigned char)t[at]) || t[at] == '.' || t[at] == 'e' || t[at] == 'E' || t[at] == '+' || t[at] == '-')) at++;
231
+ std::string piece(t.substr(start, at - start));
232
+ char* end = nullptr;
233
+ double v = std::strtod(piece.c_str(), &end);
234
+ if (!end || *end != '\0') return false;
235
+ out = Json(v);
236
+ return true;
237
+ }
238
+ static void utf8(std::string& out, uint32_t cp) {
239
+ if (cp < 0x80) out += (char)cp;
240
+ else if (cp < 0x800) { out += (char)(0xC0 | (cp >> 6)); out += (char)(0x80 | (cp & 0x3F)); }
241
+ else if (cp < 0x10000) { out += (char)(0xE0 | (cp >> 12)); out += (char)(0x80 | ((cp >> 6) & 0x3F)); out += (char)(0x80 | (cp & 0x3F)); }
242
+ else { out += (char)(0xF0 | (cp >> 18)); out += (char)(0x80 | ((cp >> 12) & 0x3F)); out += (char)(0x80 | ((cp >> 6) & 0x3F)); out += (char)(0x80 | (cp & 0x3F)); }
243
+ }
244
+ bool hex4(uint32_t& v) {
245
+ if (at + 4 > t.size()) return false;
246
+ v = 0;
247
+ for (int i = 0; i < 4; i++) {
248
+ char h = t[at++];
249
+ v <<= 4;
250
+ if (h >= '0' && h <= '9') v |= h - '0';
251
+ else if (h >= 'a' && h <= 'f') v |= h - 'a' + 10;
252
+ else if (h >= 'A' && h <= 'F') v |= h - 'A' + 10;
253
+ else return false;
254
+ }
255
+ return true;
256
+ }
257
+ bool string(std::string& out) {
258
+ at++;
259
+ while (at < t.size()) {
260
+ unsigned char c = t[at++];
261
+ if (c == '"') return true;
262
+ if (c < 0x20) return false;
263
+ if (c != '\\') { out += (char)c; continue; }
264
+ if (at >= t.size()) return false;
265
+ char e = t[at++];
266
+ switch (e) {
267
+ case '"': out += '"'; break;
268
+ case '\\': out += '\\'; break;
269
+ case '/': out += '/'; break;
270
+ case 'b': out += '\b'; break;
271
+ case 'f': out += '\f'; break;
272
+ case 'n': out += '\n'; break;
273
+ case 'r': out += '\r'; break;
274
+ case 't': out += '\t'; break;
275
+ case 'u': {
276
+ uint32_t cp;
277
+ if (!hex4(cp)) return false;
278
+ if (cp >= 0xD800 && cp < 0xDC00) {
279
+ uint32_t low;
280
+ if (at + 2 > t.size() || t[at] != '\\' || t[at + 1] != 'u') return false;
281
+ at += 2;
282
+ if (!hex4(low) || low < 0xDC00 || low > 0xDFFF) return false;
283
+ cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00);
284
+ } else if (cp >= 0xDC00 && cp < 0xE000) return false;
285
+ utf8(out, cp);
286
+ break;
287
+ }
288
+ default: return false;
289
+ }
290
+ }
291
+ return false;
292
+ }
293
+ };
294
+ };
295
+
296
+ // MARK: Bytes
297
+
298
+ inline std::string base64(std::string_view data, bool url_safe) {
299
+ static const char* plain = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
300
+ static const char* safe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
301
+ const char* table = url_safe ? safe : plain;
302
+ std::string out;
303
+ size_t i = 0;
304
+ for (; i + 2 < data.size(); i += 3) {
305
+ uint32_t v = (uint8_t)data[i] << 16 | (uint8_t)data[i + 1] << 8 | (uint8_t)data[i + 2];
306
+ out += table[v >> 18]; out += table[(v >> 12) & 63]; out += table[(v >> 6) & 63]; out += table[v & 63];
307
+ }
308
+ if (i + 1 == data.size()) {
309
+ uint32_t v = (uint8_t)data[i] << 16;
310
+ out += table[v >> 18]; out += table[(v >> 12) & 63];
311
+ if (!url_safe) out += "==";
312
+ } else if (i + 2 == data.size()) {
313
+ uint32_t v = (uint8_t)data[i] << 16 | (uint8_t)data[i + 1] << 8;
314
+ out += table[v >> 18]; out += table[(v >> 12) & 63]; out += table[(v >> 6) & 63];
315
+ if (!url_safe) out += '=';
316
+ }
317
+ return out;
318
+ }
319
+
320
+ // URL-safe base64 without padding, as ControllerToken writes it; nil for anything else.
321
+ inline std::optional<std::string> unbase64url(std::string_view text) {
322
+ std::string out;
323
+ uint32_t v = 0;
324
+ int bits = 0;
325
+ for (char c : text) {
326
+ int d;
327
+ if (c >= 'A' && c <= 'Z') d = c - 'A';
328
+ else if (c >= 'a' && c <= 'z') d = c - 'a' + 26;
329
+ else if (c >= '0' && c <= '9') d = c - '0' + 52;
330
+ else if (c == '-') d = 62;
331
+ else if (c == '_') d = 63;
332
+ else return std::nullopt;
333
+ v = v << 6 | (uint32_t)d;
334
+ bits += 6;
335
+ if (bits >= 8) { bits -= 8; out += (char)((v >> bits) & 0xFF); }
336
+ }
337
+ if (bits >= 6) return std::nullopt;
338
+ return out;
339
+ }
340
+
341
+ inline std::string random_text(size_t count) {
342
+ std::string out(count, '\0');
343
+ platform::random_bytes(reinterpret_cast<uint8_t*>(out.data()), count);
344
+ return out;
345
+ }
346
+
347
+ inline bool same(std::string_view a, std::string_view b) {
348
+ if (a.size() != b.size()) return false;
349
+ unsigned char diff = 0;
350
+ for (size_t i = 0; i < a.size(); i++) diff |= (unsigned char)(a[i] ^ b[i]);
351
+ return diff == 0;
352
+ }
353
+
354
+ inline std::string lower(std::string text) {
355
+ std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return (char)std::tolower(c); });
356
+ return text;
357
+ }
358
+
359
+ inline std::string trim(std::string_view text) {
360
+ size_t a = 0, b = text.size();
361
+ while (a < b && (text[a] == ' ' || text[a] == '\t')) a++;
362
+ while (b > a && (text[b - 1] == ' ' || text[b - 1] == '\t')) b--;
363
+ return std::string(text.substr(a, b - a));
364
+ }
365
+
366
+ // Which device a connection is from, for the limits on how many it may hold: an IPv4 address as it is, and an IPv6 one
367
+ // by its first four groups (its network), however it's written, since a device can take many addresses there.
368
+ inline std::string source_of(std::string address) {
369
+ address = lower(address.substr(0, address.find('%')));
370
+ if (address.find(':') == std::string::npos) return address;
371
+ std::vector<std::string> head, tail;
372
+ auto split = [](std::string const& text, std::vector<std::string>& into) {
373
+ size_t start = 0;
374
+ if (text.empty()) return;
375
+ for (;;) {
376
+ size_t colon = text.find(':', start);
377
+ into.push_back(text.substr(start, colon == std::string::npos ? std::string::npos : colon - start));
378
+ if (colon == std::string::npos) break;
379
+ start = colon + 1;
380
+ }
381
+ };
382
+ auto gap = address.find("::");
383
+ split(gap == std::string::npos ? address : address.substr(0, gap), head);
384
+ if (gap != std::string::npos) split(address.substr(gap + 2), tail);
385
+ // An IPv4 tail ("::ffff:1.2.3.4") counts as its two groups.
386
+ if (!tail.empty() && tail.back().find('.') != std::string::npos) { tail.back() = "0"; tail.push_back("0"); }
387
+ else if (tail.empty() && !head.empty() && head.back().find('.') != std::string::npos) { head.back() = "0"; head.push_back("0"); }
388
+ std::vector<std::string> groups = head;
389
+ while (groups.size() + tail.size() < 8) groups.push_back("0");
390
+ groups.insert(groups.end(), tail.begin(), tail.end());
391
+ std::string out;
392
+ for (size_t i = 0; i < 4 && i < groups.size(); i++) {
393
+ std::string g = groups[i];
394
+ size_t lead = g.find_first_not_of('0');
395
+ g = lead == std::string::npos ? "0" : g.substr(lead);
396
+ out += (i ? ":" : "") + g;
397
+ }
398
+ return out;
399
+ }
400
+
401
+ // MARK: Credentials
402
+
403
+ // ControllerToken: a short signed credential for one kind of holder in this session, signed with a key that lives only
404
+ // in the host's memory for the session.
405
+ // host the game's page, over the host's own link, never the network.
406
+ // join what a phone's link carries after the #: fifteen minutes.
407
+ // controller a phone's place once it has joined: two minutes, renewed while it stays.
408
+ namespace token {
409
+ enum class Kind { host, join, controller };
410
+ inline const char* name(Kind k) { return k == Kind::host ? "host" : k == Kind::join ? "join" : "controller"; }
411
+ inline int64_t lifetime_s(Kind k) { return k == Kind::host ? 12 * 3600 : k == Kind::join ? 15 * 60 : 2 * 60; }
412
+
413
+ struct Claim {
414
+ Kind kind;
415
+ std::optional<int> player;
416
+ };
417
+
418
+ inline std::string issue(Kind kind, std::string const& app, std::string_view key, std::optional<int> player = std::nullopt,
419
+ int64_t now_s = wall_ms() / 1000) {
420
+ Json payload = Json::object();
421
+ payload["k"] = name(kind);
422
+ payload["a"] = app;
423
+ payload["g"] = 0;
424
+ payload["e"] = now_s + lifetime_s(kind);
425
+ payload["n"] = base64(random_text(9), true);
426
+ if (player) payload["p"] = *player;
427
+ std::string body = base64(payload.dump(), true);
428
+ auto mac = platform::hmac_sha256(key, body);
429
+ return body + "." + base64(std::string_view(reinterpret_cast<const char*>(mac.data()), mac.size()), true);
430
+ }
431
+
432
+ inline std::optional<Claim> check(std::optional<std::string> const& credential, std::set<Kind> const& kinds, std::string const& app,
433
+ std::string_view key, int64_t now_s = wall_ms() / 1000) {
434
+ if (!credential || credential->size() > 1024) return std::nullopt;
435
+ auto dot = credential->find('.');
436
+ if (dot == std::string::npos || credential->find('.', dot + 1) != std::string::npos) return std::nullopt;
437
+ std::string body = credential->substr(0, dot);
438
+ auto signature = unbase64url(std::string_view(*credential).substr(dot + 1));
439
+ auto mac = platform::hmac_sha256(key, body);
440
+ if (!signature || !same(*signature, std::string_view(reinterpret_cast<const char*>(mac.data()), mac.size()))) return std::nullopt;
441
+ auto plain = unbase64url(body);
442
+ if (!plain) return std::nullopt;
443
+ auto payload = Json::parse(*plain);
444
+ if (!payload) return std::nullopt;
445
+ auto k = payload->str("k");
446
+ std::optional<Kind> kind;
447
+ for (Kind each : { Kind::host, Kind::join, Kind::controller }) if (k && *k == name(each)) kind = each;
448
+ if (!kind || !kinds.count(*kind) || payload->str("a") != app || payload->integer("g") != 0) return std::nullopt;
449
+ auto expires = payload->integer("e");
450
+ if (!expires || *expires <= now_s) return std::nullopt;
451
+ auto p = payload->integer("p");
452
+ return Claim{ *kind, p ? std::optional<int>((int)*p) : std::nullopt };
453
+ }
454
+ }
455
+
456
+ // MARK: The phone page
457
+
458
+ // PhonePageServer's answers: plain GETs of the page's files, from the game's package, asked for by one of the console's
459
+ // names or addresses (so a page elsewhere that made its own name lead here reads nothing), and lamp-relay.json, where
460
+ // lamp-controller.js opens its socket.
461
+ namespace page {
462
+ constexpr size_t head_limit = 16 * 1024;
463
+ constexpr int64_t head_deadline_ms = 10000;
464
+ constexpr size_t connection_limit_all = 64, connection_limit_each = 16;
465
+ constexpr size_t file_limit = 64 << 20;
466
+
467
+ struct Answer {
468
+ int status = 200;
469
+ std::string type = "text/plain; charset=utf-8";
470
+ std::string body;
471
+ bool head = false;
472
+ // What the phone's browser holds the page to (PhonePagePolicy), sent with every answer.
473
+ std::string policy = "";
474
+ };
475
+
476
+ // PhonePagePolicy's rules for the page, in the same words: nothing but the page's own files, the game's relay on the
477
+ // console by the name the phone used, at the port it listens on now, and the sites the app may reach, from
478
+ // lamp-reach.json, which Lamp writes beside the page (none when it's missing).
479
+ inline const std::string reach_file = "lamp-reach.json";
480
+
481
+ inline bool plain_host(std::string const& host) {
482
+ if (host.empty() || host.size() > 253 || host.front() == '.' || host.back() == '.' || host.find("..") != std::string::npos) return false;
483
+ return std::all_of(host.begin(), host.end(), [](char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.'; });
484
+ }
485
+
486
+ inline std::optional<std::string> policy_source(std::string text) {
487
+ std::string site = lower(trim(text));
488
+ auto split = site.find("://");
489
+ if (split == std::string::npos) return plain_host(site) ? std::optional(site) : std::nullopt;
490
+ std::string scheme = site.substr(0, split);
491
+ if (scheme != "http" && scheme != "https" && scheme != "ws" && scheme != "wss") return std::nullopt;
492
+ std::string rest = site.substr(split + 3);
493
+ if (!rest.empty() && rest.back() == '/') rest.pop_back();
494
+ std::string host = rest;
495
+ if (auto colon = rest.find(':'); colon != std::string::npos) {
496
+ std::string port = rest.substr(colon + 1);
497
+ if (port.empty() || port.size() > 5 || !std::all_of(port.begin(), port.end(), [](char c) { return c >= '0' && c <= '9'; })) return std::nullopt;
498
+ host = rest.substr(0, colon);
499
+ }
500
+ return plain_host(host) ? std::optional(scheme + "://" + rest) : std::nullopt;
501
+ }
502
+
503
+ // The reach file's sites: nullopt for an unrestricted page, which has no reach file (an app the person made, as every
504
+ // page from before these rules), none when one is there but can't be read as a list of them.
505
+ inline std::optional<std::vector<std::string>> reach(std::optional<std::string> const& file) {
506
+ if (!file) return std::nullopt;
507
+ std::vector<std::string> only;
508
+ auto json = Json::parse(*file);
509
+ if (!json) return only;
510
+ if (auto list = json->get("only"); list && list->type == Json::Type::array)
511
+ for (auto const& item : list->a) if (item.type == Json::Type::string) only.push_back(item.s);
512
+ return only;
513
+ }
514
+
515
+ // Empty for an unrestricted page, which goes out with no header.
516
+ inline std::string policy(std::optional<std::string> const& host, std::optional<uint16_t> relay_port,
517
+ std::optional<std::vector<std::string>> const& named) {
518
+ if (!named) return "";
519
+ std::vector<std::string> sites;
520
+ for (auto const& site : *named) if (auto source = policy_source(site)) sites.push_back(*source);
521
+ std::string connect = "'self'";
522
+ if (host) {
523
+ std::string name = lower(trim(*host));
524
+ if (!name.empty() && name[0] != '[') {
525
+ name = name.substr(0, name.find(':'));
526
+ if (plain_host(name) && relay_port) connect += " ws://" + name + ":" + std::to_string(*relay_port);
527
+ }
528
+ }
529
+ auto list = [&](std::string base) { for (auto const& site : sites) base += " " + site; return base; };
530
+ return "default-src 'none'; script-src " + list("'self' 'unsafe-inline'") + "; style-src " + list("'self' 'unsafe-inline'")
531
+ + "; img-src " + list("'self' data: blob:") + "; media-src " + list("'self' data: blob:") + "; font-src " + list("'self' data:")
532
+ + "; connect-src " + list(connect) + "; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'; form-action 'none'";
533
+ }
534
+
535
+ inline std::string type_for(std::string ext) {
536
+ ext = lower(ext);
537
+ static const std::map<std::string, std::string> types = {
538
+ { "html", "text/html; charset=utf-8" }, { "htm", "text/html; charset=utf-8" }, { "js", "text/javascript; charset=utf-8" },
539
+ { "mjs", "text/javascript; charset=utf-8" }, { "css", "text/css; charset=utf-8" }, { "json", "application/json" },
540
+ { "webmanifest", "application/json" }, { "svg", "image/svg+xml" }, { "png", "image/png" }, { "jpg", "image/jpeg" },
541
+ { "jpeg", "image/jpeg" }, { "gif", "image/gif" }, { "webp", "image/webp" }, { "ico", "image/x-icon" }, { "woff2", "font/woff2" },
542
+ { "woff", "font/woff" }, { "mp3", "audio/mpeg" }, { "wav", "audio/wav" }, { "mp4", "video/mp4" }, { "txt", "text/plain; charset=utf-8" } };
543
+ auto found = types.find(ext);
544
+ return found == types.end() ? "application/octet-stream" : found->second;
545
+ }
546
+
547
+ // Whether a Host header names one of `hosts`, with at most a numeric port after it.
548
+ inline bool names(std::optional<std::string> const& header, std::set<std::string> const& hosts) {
549
+ if (!header) return false;
550
+ std::string host = lower(trim(*header));
551
+ if (host.empty()) return false;
552
+ auto digits = [](std::string_view s) { return !s.empty() && std::all_of(s.begin(), s.end(), [](char c) { return c >= '0' && c <= '9'; }); };
553
+ if (host[0] == '[') {
554
+ auto close = host.find(']');
555
+ if (close == std::string::npos) return false;
556
+ std::string port = host.substr(close + 1);
557
+ if (!port.empty() && !(port[0] == ':' && digits(std::string_view(port).substr(1)))) return false;
558
+ host = host.substr(1, close - 1);
559
+ } else if (auto colon = host.find(':'); colon != std::string::npos) {
560
+ if (!digits(std::string_view(host).substr(colon + 1))) return false;
561
+ host = host.substr(0, colon);
562
+ }
563
+ return hosts.count(host) > 0;
564
+ }
565
+
566
+ // A step of a path a page may serve: ControllerSnapshot.isServed's rule (a name, not hidden, not empty), and nothing
567
+ // Windows reads as something other than a file's own name: a stream (":"), a name it trims (a trailing dot or space), a
568
+ // device, or a short name ("HIDDEN~1"), which could name a file the long rules turn away.
569
+ inline bool served_step(std::string const& step) {
570
+ if (step.empty() || step[0] == '.' || step.size() > 255) return false;
571
+ for (size_t i = 0; i + 1 < step.size(); i++) if (step[i] == '~' && step[i + 1] >= '0' && step[i + 1] <= '9') return false;
572
+ if (step.find_first_of(std::string("\\:\0", 3)) != std::string::npos) return false;
573
+ if (step.back() == '.' || step.back() == ' ') return false;
574
+ for (unsigned char c : step) if (c < 0x20) return false;
575
+ std::string stem = lower(step.substr(0, step.find('.')));
576
+ static const std::set<std::string> devices = { "con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7",
577
+ "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9" };
578
+ return !devices.count(stem);
579
+ }
580
+
581
+ inline std::optional<std::string> percent_decoded(std::string_view text) {
582
+ std::string out;
583
+ for (size_t i = 0; i < text.size(); i++) {
584
+ if (text[i] != '%') { out += text[i]; continue; }
585
+ if (i + 2 >= text.size()) return std::nullopt;
586
+ auto hex = [](char c) -> int { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1; };
587
+ int hi = hex(text[i + 1]), lo = hex(text[i + 2]);
588
+ if (hi < 0 || lo < 0) return std::nullopt;
589
+ out += (char)(hi << 4 | lo);
590
+ i += 2;
591
+ }
592
+ return out;
593
+ }
594
+
595
+ // Reads a file of the page by its steps, each already checked; nil when there's no such plain file.
596
+ using Reader = std::function<std::optional<std::string>(std::vector<std::string> const& steps)>;
597
+
598
+ // `held`: the page has rules, which lamp-relay.json says (policy), so lamp-controller.js only ever takes a refused socket
599
+ // for a moved relay on a page given rules.
600
+ inline Answer answer_path(std::string const& full, std::optional<std::string> const& host, std::set<std::string> const& hosts,
601
+ std::optional<uint16_t> relay_port, std::string const& app, Reader const& read, bool held = false) {
602
+ if (!names(host, hosts)) return { 403, "text/plain; charset=utf-8", "Not for this host." };
603
+ Answer missing{ 404, "text/plain; charset=utf-8", "Nothing here. Open the game first." };
604
+ std::string path = full.substr(0, full.find('?'));
605
+ if (path.empty() || path[0] != '/') return missing;
606
+ std::vector<std::string> steps;
607
+ size_t start = 1;
608
+ for (;;) {
609
+ size_t slash = path.find('/', start);
610
+ auto decoded = percent_decoded(std::string_view(path).substr(start, slash == std::string::npos ? std::string::npos : slash - start));
611
+ if (!decoded || decoded->find('/') != std::string::npos) return missing;
612
+ steps.push_back(*decoded);
613
+ if (slash == std::string::npos) break;
614
+ start = slash + 1;
615
+ }
616
+ if (steps.back().empty()) steps.back() = "index.html";
617
+ if (steps.size() == 1 && steps[0] == "lamp-relay.json") {
618
+ if (!relay_port) return { 503, "text/plain; charset=utf-8", "The link for controllers isn't open." };
619
+ Json body = Json::object();
620
+ body["port"] = (int)*relay_port;
621
+ body["app"] = app;
622
+ if (held) body["policy"] = true;
623
+ return { 200, type_for("json"), body.dump() };
624
+ }
625
+ size_t length = 0;
626
+ for (auto const& step : steps) { length += step.size() + 1; if (!served_step(step)) return missing; }
627
+ if (length > 1024) return missing;
628
+ auto data = read(steps);
629
+ if (!data || data->size() > file_limit) return missing;
630
+ auto dot = steps.back().rfind('.');
631
+ return { 200, type_for(dot == std::string::npos ? "" : steps.back().substr(dot + 1)), std::move(*data) };
632
+ }
633
+
634
+ inline std::optional<std::string> header(std::vector<std::string> const& lines, std::string const& name) {
635
+ for (size_t i = 1; i < lines.size(); i++) {
636
+ auto colon = lines[i].find(':');
637
+ if (colon != std::string::npos && lower(lines[i].substr(0, colon)) == name) return trim(std::string_view(lines[i]).substr(colon + 1));
638
+ }
639
+ return std::nullopt;
640
+ }
641
+
642
+ inline std::vector<std::string> lines_of(std::string_view head) {
643
+ std::vector<std::string> lines;
644
+ size_t start = 0;
645
+ for (;;) {
646
+ size_t end = head.find("\r\n", start);
647
+ lines.emplace_back(head.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start));
648
+ if (end == std::string_view::npos) break;
649
+ start = end + 2;
650
+ }
651
+ return lines;
652
+ }
653
+
654
+ // The answer to a request's head (its request line and headers, without the blank line).
655
+ // Whether anything is at a page's steps, readable or not, so a reach file that's there but can't be read (`Reader` gives
656
+ // nothing for both) holds the page to no sites rather than none at all. A host that gives none takes every reach file as
657
+ // there: a page fails closed.
658
+ using Exists = std::function<bool(std::vector<std::string> const& steps)>;
659
+
660
+ inline Answer answer(std::string_view head, std::set<std::string> const& hosts, std::optional<uint16_t> relay_port,
661
+ std::string const& app, Reader const& read, Exists const& exists = {}) {
662
+ auto lines = lines_of(head);
663
+ std::string const& first = lines[0];
664
+ auto space = first.find(' ');
665
+ if (space == std::string::npos) return { 400, "text/plain; charset=utf-8", "Bad request" };
666
+ std::string method = first.substr(0, space);
667
+ auto end = first.find(' ', space + 1);
668
+ std::string target = first.substr(space + 1, end == std::string::npos ? std::string::npos : end - space - 1);
669
+ if (method != "GET" && method != "HEAD") return { 405, "text/plain; charset=utf-8", "Not allowed" };
670
+ auto host = header(lines, "host");
671
+ auto file = read({ reach_file });
672
+ auto sites = file || !exists || exists({ reach_file }) ? reach(file ? file : std::optional<std::string>("")) : std::nullopt;
673
+ auto page = answer_path(target, host, hosts, relay_port, app, read, sites.has_value());
674
+ page.head = method == "HEAD";
675
+ if (names(host, hosts)) page.policy = policy(host, relay_port, sites);
676
+ return page;
677
+ }
678
+
679
+ inline std::string response(Answer const& page) {
680
+ const char* reason = page.status == 200 ? "OK" : page.status == 400 ? "Bad Request" : page.status == 403 ? "Forbidden"
681
+ : page.status == 404 ? "Not Found" : page.status == 405 ? "Method Not Allowed" : "Service Unavailable";
682
+ std::string out = "HTTP/1.1 " + std::to_string(page.status) + " " + reason + "\r\nContent-Type: " + page.type
683
+ + "\r\nContent-Length: " + std::to_string(page.body.size())
684
+ + (page.policy.empty() ? "" : "\r\nContent-Security-Policy: " + page.policy)
685
+ + "\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nConnection: close\r\n\r\n";
686
+ if (!page.head) out += page.body;
687
+ return out;
688
+ }
689
+ }
690
+
691
+ // MARK: WebSocket
692
+
693
+ namespace ws {
694
+ enum Op : uint8_t { continuation = 0, text = 1, binary = 2, close = 8, ping = 9, pong = 10 };
695
+
696
+ inline std::string accept_key(std::string const& key) {
697
+ auto digest = platform::sha1(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
698
+ return base64(std::string_view(reinterpret_cast<const char*>(digest.data()), digest.size()), false);
699
+ }
700
+
701
+ // A server's frame: never masked, never split.
702
+ inline std::string frame(uint8_t op, std::string_view payload) {
703
+ std::string out;
704
+ out += (char)(0x80 | op);
705
+ if (payload.size() < 126) out += (char)payload.size();
706
+ else if (payload.size() < 65536) { out += (char)126; out += (char)(payload.size() >> 8); out += (char)(payload.size() & 0xFF); }
707
+ else { out += (char)127; for (int i = 7; i >= 0; i--) out += (char)((uint64_t)payload.size() >> (i * 8) & 0xFF); }
708
+ out += payload;
709
+ return out;
710
+ }
711
+
712
+ inline std::string close_frame(int code) {
713
+ std::string payload;
714
+ payload += (char)((code >> 8) & 0xFF);
715
+ payload += (char)(code & 0xFF);
716
+ return frame(close, payload);
717
+ }
718
+
719
+ inline bool valid_utf8(std::string_view s) {
720
+ size_t i = 0;
721
+ while (i < s.size()) {
722
+ unsigned char c = s[i];
723
+ size_t n = c < 0x80 ? 0 : (c >> 5) == 6 ? 1 : (c >> 4) == 14 ? 2 : (c >> 3) == 30 ? 3 : 9;
724
+ if (n == 9 || i + n >= s.size()) return false;
725
+ uint32_t cp = n == 0 ? c : n == 1 ? c & 0x1F : n == 2 ? c & 0x0F : c & 0x07;
726
+ for (size_t k = 1; k <= n; k++) {
727
+ if (i + k >= s.size() || ((unsigned char)s[i + k] >> 6) != 2) return false;
728
+ cp = cp << 6 | ((unsigned char)s[i + k] & 0x3F);
729
+ }
730
+ if ((n == 1 && cp < 0x80) || (n == 2 && cp < 0x800) || (n == 3 && cp < 0x10000) || cp > 0x10FFFF || (cp >= 0xD800 && cp < 0xE000)) return false;
731
+ i += n + 1;
732
+ }
733
+ return true;
734
+ }
735
+
736
+ // Reads a client's frames: each must be masked, and a message held to `limit` bytes. What it read is a message, a
737
+ // ping, a close, or a reason to let the socket go (a close code).
738
+ struct Reader {
739
+ struct Event {
740
+ enum Kind { message, ping, closed, failed } kind;
741
+ uint8_t op = 0;
742
+ std::string data;
743
+ int code = 0;
744
+ };
745
+ size_t limit = wire::message_limit;
746
+ std::string buffer;
747
+ std::string partial;
748
+ uint8_t partial_op = 0;
749
+
750
+ // Takes what arrived; the events it completes, in order. Nothing more is read after a close or a failure.
751
+ std::vector<Event> feed(std::string_view bytes) {
752
+ std::vector<Event> events;
753
+ buffer.append(bytes.data(), bytes.size());
754
+ for (;;) {
755
+ if (buffer.size() < 2) break;
756
+ uint8_t b0 = buffer[0], b1 = buffer[1];
757
+ bool fin = b0 & 0x80;
758
+ uint8_t op = b0 & 0x0F;
759
+ if (b0 & 0x70) return fail(events, 1002);
760
+ if (!(b1 & 0x80)) return fail(events, 1002);
761
+ uint64_t length = b1 & 0x7F;
762
+ size_t at = 2;
763
+ if (length == 126) {
764
+ if (buffer.size() < 4) break;
765
+ length = (uint8_t)buffer[2] << 8 | (uint8_t)buffer[3];
766
+ at = 4;
767
+ } else if (length == 127) {
768
+ if (buffer.size() < 10) break;
769
+ length = 0;
770
+ for (int i = 0; i < 8; i++) length = length << 8 | (uint8_t)buffer[2 + i];
771
+ at = 10;
772
+ }
773
+ bool control = op & 0x08;
774
+ if (control && (length > 125 || !fin)) return fail(events, 1002);
775
+ // Checked before anything is added to it, so a length near 2^64 can't wrap past the limit.
776
+ if (!control && (length > limit || partial.size() > limit - length)) return fail(events, 1009);
777
+ if (buffer.size() < at + 4 + length) break;
778
+ const char* mask = buffer.data() + at;
779
+ std::string payload = buffer.substr(at + 4, (size_t)length);
780
+ for (size_t i = 0; i < payload.size(); i++) payload[i] ^= mask[i % 4];
781
+ buffer.erase(0, at + 4 + (size_t)length);
782
+ if (op == close) {
783
+ int code = payload.size() >= 2 ? ((uint8_t)payload[0] << 8 | (uint8_t)payload[1]) : 1005;
784
+ events.push_back({ Event::closed, op, {}, code });
785
+ buffer.clear();
786
+ return events;
787
+ }
788
+ if (op == ping) { events.push_back({ Event::ping, op, std::move(payload) }); continue; }
789
+ if (op == pong) continue;
790
+ if (op == continuation) {
791
+ if (!partial_op) return fail(events, 1002);
792
+ partial += payload;
793
+ } else if (op == text || op == binary) {
794
+ if (partial_op) return fail(events, 1002);
795
+ partial_op = op;
796
+ partial = std::move(payload);
797
+ } else return fail(events, 1002);
798
+ if (!fin) continue;
799
+ if (partial_op == text && !valid_utf8(partial)) return fail(events, 1007);
800
+ events.push_back({ Event::message, partial_op, std::move(partial) });
801
+ partial.clear();
802
+ partial_op = 0;
803
+ }
804
+ return events;
805
+ }
806
+
807
+ std::vector<Event> fail(std::vector<Event>& events, int code) {
808
+ events.push_back({ Event::failed, 0, {}, code });
809
+ buffer.clear();
810
+ return events;
811
+ }
812
+ };
813
+
814
+ struct Upgrade {
815
+ std::string key;
816
+ std::optional<std::string> origin;
817
+ };
818
+
819
+ // A request's head that asks for a WebSocket, nil for anything else.
820
+ inline std::optional<Upgrade> upgrade(std::string_view head) {
821
+ auto lines = page::lines_of(head);
822
+ if (lines[0].rfind("GET ", 0) != 0) return std::nullopt;
823
+ auto key = page::header(lines, "sec-websocket-key");
824
+ auto version = page::header(lines, "sec-websocket-version");
825
+ auto up = page::header(lines, "upgrade");
826
+ if (!key || key->empty() || key->size() > 64 || version != std::string("13") || !up || lower(*up) != "websocket") return std::nullopt;
827
+ return Upgrade{ *key, page::header(lines, "origin") };
828
+ }
829
+
830
+ inline std::string accepted(std::string const& key) {
831
+ return "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + accept_key(key) + "\r\n\r\n";
832
+ }
833
+
834
+ inline std::string refused(int status) {
835
+ return "HTTP/1.1 " + std::to_string(status) + (status == 403 ? " Forbidden" : " Bad Request")
836
+ + "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
837
+ }
838
+ }
839
+
840
+ // MARK: Peers
841
+
842
+ // One end the relay talks to: a phone's WebSocket, or the game's page over the host's own link.
843
+ struct Peer {
844
+ virtual ~Peer() = default;
845
+ virtual void send_text(std::string const& text) = 0;
846
+ // Queues a piece of a file; false when the peer already has more waiting than it could ever be owed.
847
+ virtual bool send_piece(std::string piece) = 0;
848
+ virtual void close(int code) = 0;
849
+ virtual bool is_open() const = 0;
850
+ // Text not yet handed on: how far behind the other end is.
851
+ virtual size_t pending_text() const = 0;
852
+ int64_t heard = wall_ms();
853
+ std::function<void(std::string const&)> on_text = [](std::string const&) {};
854
+ std::function<void(std::string const&)> on_binary = [](std::string const&) {};
855
+ std::function<void()> on_close = [] {};
856
+ };
857
+
858
+ // What a WebSocket is carried on: writes go one at a time, `done` called once each has gone (or failed), on the
859
+ // relay's thread; `shutdown` lets the socket go now.
860
+ struct Transport {
861
+ virtual ~Transport() = default;
862
+ virtual void write(std::string bytes, std::function<void(bool ok)> done) = 0;
863
+ virtual void shutdown() = 0;
864
+ };
865
+
866
+ // RelayPeer: a WebSocket past its handshake. Text goes before any file's piece waiting, so a button pressed meanwhile
867
+ // waits behind one piece at most.
868
+ struct SocketPeer : Peer, std::enable_shared_from_this<SocketPeer> {
869
+ static constexpr size_t text_limit_messages = 256, text_limit_bytes = 2 * 1024 * 1024;
870
+ static constexpr size_t piece_limit = wire::players * wire::transfers * (wire::window + wire::chunk + wire::header);
871
+
872
+ std::unique_ptr<Transport> transport;
873
+ ws::Reader reader;
874
+ bool open = true;
875
+ bool writing = false;
876
+ // Closing: the close frame is on its way, after which the socket goes; or since when, to let it go anyway.
877
+ int64_t closing_since = 0;
878
+ std::deque<std::string> texts;
879
+ size_t text_bytes = 0;
880
+ std::deque<std::string> pieces;
881
+ size_t piece_bytes = 0;
882
+ std::optional<int> closing_code;
883
+
884
+ explicit SocketPeer(std::unique_ptr<Transport> t) : transport(std::move(t)) {}
885
+
886
+ bool is_open() const override { return open; }
887
+ size_t pending_text() const override { return texts.size(); }
888
+
889
+ void send_text(std::string const& text) override { queue_text(ws::frame(ws::text, text)); }
890
+
891
+ void queue_text(std::string framed) {
892
+ if (!open) return;
893
+ if (texts.size() >= text_limit_messages || text_bytes + framed.size() > text_limit_bytes) return close(4002);
894
+ text_bytes += framed.size();
895
+ texts.push_back(std::move(framed));
896
+ pump();
897
+ }
898
+
899
+ bool send_piece(std::string piece) override {
900
+ if (!open || piece_bytes + piece.size() > piece_limit) return false;
901
+ piece_bytes += piece.size();
902
+ pieces.push_back(std::move(piece));
903
+ pump();
904
+ return true;
905
+ }
906
+
907
+ void pump() {
908
+ if (writing || !transport) return;
909
+ std::string next;
910
+ bool is_text = false;
911
+ if (!texts.empty()) { next = std::move(texts.front()); texts.pop_front(); is_text = true; }
912
+ else if (open && !pieces.empty()) { next = ws::frame(ws::binary, pieces.front()); piece_bytes -= pieces.front().size(); pieces.pop_front(); }
913
+ else if (!open && closing_code) {
914
+ std::string frame = ws::close_frame(*closing_code);
915
+ closing_code.reset();
916
+ writing = true;
917
+ auto self = shared_from_this();
918
+ transport->write(frame, [self](bool) { self->writing = false; self->finish(); });
919
+ return;
920
+ } else return;
921
+ size_t size = next.size();
922
+ writing = true;
923
+ auto self = shared_from_this();
924
+ transport->write(std::move(next), [self, is_text, size](bool ok) {
925
+ self->writing = false;
926
+ if (is_text) self->text_bytes -= std::min(self->text_bytes, size);
927
+ if (!ok) return self->lost();
928
+ self->pump();
929
+ });
930
+ }
931
+
932
+ // Says why, and lets the socket go once that has gone (or, for a peer that has stopped reading, soon after: `tick`).
933
+ void close(int code) override {
934
+ if (!open) return;
935
+ open = false;
936
+ texts.clear();
937
+ text_bytes = 0;
938
+ pieces.clear();
939
+ piece_bytes = 0;
940
+ closing_code = code;
941
+ closing_since = wall_ms();
942
+ pump();
943
+ on_close();
944
+ }
945
+
946
+ void finish() {
947
+ if (transport) transport->shutdown();
948
+ transport.reset();
949
+ }
950
+
951
+ // The socket went away under it.
952
+ void lost() {
953
+ bool was = open;
954
+ open = false;
955
+ texts.clear();
956
+ pieces.clear();
957
+ closing_code.reset();
958
+ finish();
959
+ if (was) on_close();
960
+ }
961
+
962
+ // A close that hasn't gone after two seconds lets the socket go anyway.
963
+ void tick(int64_t now) {
964
+ if (!open && transport && closing_since && now - closing_since > 2000) finish();
965
+ }
966
+
967
+ void received(std::string_view bytes) {
968
+ if (!open) return;
969
+ for (auto& event : reader.feed(bytes)) {
970
+ if (!open) return;
971
+ heard = wall_ms();
972
+ switch (event.kind) {
973
+ case ws::Reader::Event::message:
974
+ if (event.op == ws::text) {
975
+ if (event.data.size() > wire::message) return close(1009);
976
+ on_text(event.data);
977
+ } else on_binary(event.data);
978
+ break;
979
+ case ws::Reader::Event::ping:
980
+ queue_text(ws::frame(ws::pong, event.data));
981
+ break;
982
+ case ws::Reader::Event::closed:
983
+ return close(1000);
984
+ case ws::Reader::Event::failed:
985
+ return close(event.code);
986
+ }
987
+ }
988
+ }
989
+ };
990
+
991
+ // MARK: The relay
992
+
993
+ // Relay's game part, for the one game this host runs: its page (the host, over the host's own link) and up to eight
994
+ // phones, each at a place 1…8. Phones send buttons, sticks and messages to the page, and files both ways in pieces with
995
+ // their own flow control; nothing is kept once it's passed on.
996
+ class Relay {
997
+ public:
998
+ struct Transfer {
999
+ int64_t size;
1000
+ int64_t passed = 0, acknowledged = 0, unacknowledged = 0;
1001
+ explicit Transfer(int64_t s) : size(s) {}
1002
+ bool acknowledge(int64_t got) {
1003
+ if (size == 0 && got == 0 && acknowledged == 0 && unacknowledged == 0) { unacknowledged = -1; return true; }
1004
+ if (got <= acknowledged || got > passed || unacknowledged <= 0) return false;
1005
+ acknowledged = got;
1006
+ unacknowledged -= 1;
1007
+ return true;
1008
+ }
1009
+ };
1010
+
1011
+ // About as often as a screen draws; past that messages are dropped, and a peer that goes on for ten seconds goes.
1012
+ struct Rate {
1013
+ double tokens = wire::per_second;
1014
+ int64_t refilled;
1015
+ int64_t over = 0, last_dropped = 0;
1016
+ explicit Rate(int64_t now) : refilled(now) {}
1017
+ bool allowed(Peer* peer, int64_t now) {
1018
+ tokens = std::min(wire::per_second, tokens + (double)(now - refilled) / 1000.0 * wire::per_second);
1019
+ refilled = now;
1020
+ if (now - last_dropped > 2000) over = 0;
1021
+ if (tokens >= 1) { tokens -= 1; return true; }
1022
+ last_dropped = now;
1023
+ if (!over) over = now;
1024
+ if (now - over > 10000 && peer) peer->close(4008);
1025
+ return false;
1026
+ }
1027
+ };
1028
+
1029
+ struct Player {
1030
+ std::shared_ptr<Peer> peer;
1031
+ int64_t held_until = 0;
1032
+ Rate rate;
1033
+ std::map<uint32_t, Transfer> to_host, to_phone;
1034
+ Player(std::shared_ptr<Peer> p, int64_t now) : peer(std::move(p)), rate(now) {}
1035
+ };
1036
+
1037
+ static constexpr size_t greeting_all = 64, greeting_each = 8;
1038
+ static constexpr int64_t greeting_ms = 5000;
1039
+ static inline size_t host_behind = 128;
1040
+
1041
+ std::function<int64_t()> now = [] { return wall_ms(); };
1042
+
1043
+ Relay(std::string app, std::string key) : app_(std::move(app)), key_(std::move(key)) {}
1044
+
1045
+ std::string const& app() const { return app_; }
1046
+
1047
+ // Where the page is now: the address phones open, its port, and the console's names and addresses. A page that moved
1048
+ // is told its new link.
1049
+ void configure(std::optional<std::string> page, std::optional<uint16_t> page_port, std::set<std::string> hosts) {
1050
+ bool moved = page != page_;
1051
+ page_ = std::move(page);
1052
+ page_port_ = page_port;
1053
+ hosts_ = std::move(hosts);
1054
+ if (moved && host_) send_link();
1055
+ }
1056
+
1057
+ // A browser always says which page opened a socket: only the game's own phone page, at its port, by one of the
1058
+ // console's names. A client that isn't a browser could say anything, so its credential is what lets it in.
1059
+ bool admits(std::optional<std::string> const& origin) const {
1060
+ if (!origin) return true;
1061
+ std::string o = lower(*origin);
1062
+ if (o.rfind("http://", 0) != 0) return false;
1063
+ std::string rest = o.substr(7);
1064
+ if (rest.find('/') != std::string::npos || rest.find('@') != std::string::npos) return false;
1065
+ size_t colon = rest.rfind(':');
1066
+ if (colon == std::string::npos || (rest[0] == '[' && colon < rest.find(']'))) return false;
1067
+ std::string port = rest.substr(colon + 1), host = rest.substr(0, colon);
1068
+ if (port.empty() || port.size() > 5 || !std::all_of(port.begin(), port.end(), [](char c) { return c >= '0' && c <= '9'; })) return false;
1069
+ if (host.size() > 2 && host.front() == '[' && host.back() == ']') host = host.substr(1, host.size() - 2);
1070
+ return page_port_ && std::stoi(port) == *page_port_ && hosts_.count(host);
1071
+ }
1072
+
1073
+ std::string host_credential() const { return token::issue(token::Kind::host, app_, key_); }
1074
+
1075
+ // A socket to hear from: false when too many wait to say hello, in all or from `from`. `trusted`: the page's own link
1076
+ // through the host, the only one that may be the game's host.
1077
+ bool accept(std::shared_ptr<Peer> peer, std::string const& from, bool trusted) {
1078
+ if (stopped_) return false;
1079
+ size_t mine = 0;
1080
+ for (auto const& g : greeting_) if (g.second.from == from) mine++;
1081
+ if (greeting_.size() >= greeting_all || mine >= greeting_each) return false;
1082
+ Peer* id = peer.get();
1083
+ greeting_[id] = { peer, from, now() };
1084
+ std::weak_ptr<Peer> weak = peer;
1085
+ peer->on_close = [this, id] { greeting_.erase(id); };
1086
+ peer->on_binary = [weak](std::string const&) { if (auto p = weak.lock()) p->close(4003); };
1087
+ peer->on_text = [this, id, weak, trusted](std::string const& text) {
1088
+ auto p = weak.lock();
1089
+ if (!p || !greeting_.count(id)) return;
1090
+ greeting_.erase(id);
1091
+ hello(p, text, trusted);
1092
+ };
1093
+ return true;
1094
+ }
1095
+
1096
+ // Every second: hellos that never came, and every five seconds a beat on every socket (with the relay's time), fresh
1097
+ // credentials for the phones each minute, and sockets gone quiet let go.
1098
+ void tick() {
1099
+ int64_t t = now();
1100
+ std::vector<std::shared_ptr<Peer>> late;
1101
+ for (auto const& g : greeting_) if (t - g.second.since > greeting_ms) late.push_back(g.second.peer);
1102
+ for (auto& p : late) { greeting_.erase(p.get()); p->close(4001); }
1103
+ if (t - last_beat_ < wire::beat_ms) return;
1104
+ last_beat_ = t;
1105
+ Json b = Json::object();
1106
+ b["t"] = "beat";
1107
+ b["now"] = t;
1108
+ std::string beat = b.dump();
1109
+ bool renew = t - renewed_ >= 60000;
1110
+ if (renew) renewed_ = t;
1111
+ std::vector<int> gone;
1112
+ for (auto& [number, player] : players_) {
1113
+ if (!player.peer) { if (player.held_until < t) gone.push_back(number); continue; }
1114
+ auto peer = player.peer;
1115
+ if (t - peer->heard > wire::silence_ms) { peer->close(4002); continue; }
1116
+ peer->send_text(beat);
1117
+ if (renew) {
1118
+ Json c = Json::object();
1119
+ c["t"] = "credential";
1120
+ c["credential"] = token::issue(token::Kind::controller, app_, key_, number, t / 1000);
1121
+ peer->send_text(c.dump());
1122
+ }
1123
+ }
1124
+ for (int n : gone) players_.erase(n);
1125
+ if (host_) {
1126
+ if (t - host_->heard > wire::silence_ms) host_->close(4002);
1127
+ else host_->send_text(beat);
1128
+ }
1129
+ }
1130
+
1131
+ // The session is over: every phone and the page are let go, and nothing more is taken.
1132
+ void stop() {
1133
+ stopped_ = true;
1134
+ std::vector<std::shared_ptr<Peer>> all;
1135
+ for (auto& [n, player] : players_) if (player.peer) all.push_back(player.peer);
1136
+ players_.clear();
1137
+ if (host_) all.push_back(host_);
1138
+ host_.reset();
1139
+ for (auto& p : all) { p->on_close = [] {}; p->close(4005); }
1140
+ std::vector<std::shared_ptr<Peer>> waiting;
1141
+ for (auto& g : greeting_) waiting.push_back(g.second.peer);
1142
+ greeting_.clear();
1143
+ for (auto& p : waiting) { p->on_close = [] {}; p->close(1001); }
1144
+ }
1145
+
1146
+ // For tests and the log.
1147
+ std::vector<int> connected() const {
1148
+ std::vector<int> list;
1149
+ for (auto const& [n, player] : players_) if (player.peer) list.push_back(n);
1150
+ return list;
1151
+ }
1152
+ bool has_host() const { return host_ != nullptr; }
1153
+
1154
+ private:
1155
+ struct Waiting {
1156
+ std::shared_ptr<Peer> peer;
1157
+ std::string from;
1158
+ int64_t since;
1159
+ };
1160
+
1161
+ std::string app_, key_;
1162
+ std::optional<std::string> page_;
1163
+ std::optional<uint16_t> page_port_;
1164
+ std::set<std::string> hosts_;
1165
+ std::map<Peer*, Waiting> greeting_;
1166
+ std::map<int, Player> players_;
1167
+ std::shared_ptr<Peer> host_;
1168
+ int64_t renewed_ = wall_ms(), last_beat_ = wall_ms();
1169
+ bool stopped_ = false;
1170
+
1171
+ static std::optional<Json> parse(std::string const& text) {
1172
+ if (text.size() > wire::message) return std::nullopt;
1173
+ auto j = Json::parse(text);
1174
+ if (!j || j->type != Json::Type::object) return std::nullopt;
1175
+ return j;
1176
+ }
1177
+ static std::string shorter(Json const& m, std::string const& key, size_t limit) {
1178
+ auto s = m.str(key).value_or("");
1179
+ // Cut at a character's start, as Swift's prefix counts characters, so what's passed on stays UTF-8.
1180
+ if (s.size() <= limit) return s;
1181
+ size_t cut = limit;
1182
+ while (cut > 0 && ((unsigned char)s[cut] & 0xC0) == 0x80) cut--;
1183
+ return s.substr(0, cut);
1184
+ }
1185
+ static std::optional<uint32_t> id_of(Json const& m) {
1186
+ auto n = m.integer("id");
1187
+ if (!n || *n <= 0 || *n >= (int64_t(1) << 31)) return std::nullopt;
1188
+ return (uint32_t)*n;
1189
+ }
1190
+ static std::optional<std::pair<uint8_t, uint32_t>> chunk_header(std::string const& data) {
1191
+ if (data.size() < wire::header || (uint8_t)data[0] != 1) return std::nullopt;
1192
+ uint32_t id = 0;
1193
+ for (int i = 4; i < 8; i++) id = id << 8 | (uint8_t)data[i];
1194
+ return std::make_pair((uint8_t)data[1], id);
1195
+ }
1196
+ static std::string readdressed(std::string data, uint8_t player) {
1197
+ data[1] = (char)player;
1198
+ return data;
1199
+ }
1200
+ static Json msg(const char* t) {
1201
+ Json m = Json::object();
1202
+ m["t"] = t;
1203
+ return m;
1204
+ }
1205
+
1206
+ void hello(std::shared_ptr<Peer> const& peer, std::string const& text, bool trusted) {
1207
+ auto m = parse(text);
1208
+ if (!m || m->str("t") != std::string("hello") || m->str("app") != app_) return peer->close(4003);
1209
+ std::set<token::Kind> kinds = { token::Kind::join, token::Kind::controller };
1210
+ if (trusted) kinds.insert(token::Kind::host);
1211
+ auto credential = m->get("credential");
1212
+ auto claim = token::check(credential && credential->type == Json::Type::string ? std::optional<std::string>(credential->s) : std::nullopt,
1213
+ kinds, app_, key_, now() / 1000);
1214
+ if (!claim) return peer->close(4003);
1215
+ if (claim->kind == token::Kind::host) return join_as_host(peer);
1216
+ join_as_player(peer, claim->kind == token::Kind::controller ? claim->player : std::nullopt);
1217
+ }
1218
+
1219
+ void join_as_host(std::shared_ptr<Peer> const& peer) {
1220
+ if (auto old = host_) {
1221
+ old->on_close = [] {};
1222
+ old->close(4000);
1223
+ drop_transfers();
1224
+ for (auto& [n, player] : players_) if (player.peer) player.peer->send_text(R"({"host":false,"t":"state"})");
1225
+ }
1226
+ host_ = peer;
1227
+ Peer* raw = peer.get();
1228
+ peer->on_text = [this](std::string const& text) { if (auto m = parse(text)) from_host(*m); };
1229
+ peer->on_binary = [this](std::string const& data) { piece_from_host(data); };
1230
+ peer->on_close = [this, raw] {
1231
+ if (host_.get() != raw) return;
1232
+ host_.reset();
1233
+ drop_transfers();
1234
+ for (auto& [n, player] : players_) if (player.peer) player.peer->send_text(R"({"host":false,"t":"state"})");
1235
+ };
1236
+ Json welcome = msg("welcome");
1237
+ welcome["role"] = "host";
1238
+ Json list = Json::array();
1239
+ for (int n : connected()) list.a.push_back(n);
1240
+ welcome["players"] = list;
1241
+ welcome["now"] = now();
1242
+ peer->send_text(welcome.dump());
1243
+ for (auto& [n, player] : players_) if (player.peer) player.peer->send_text(R"({"host":true,"t":"state"})");
1244
+ }
1245
+
1246
+ void join_as_player(std::shared_ptr<Peer> const& peer, std::optional<int> resuming) {
1247
+ int64_t t = now();
1248
+ std::optional<int> number = resuming;
1249
+ if (resuming && (*resuming < 1 || *resuming > wire::players)) return peer->close(4003);
1250
+ if (resuming) {
1251
+ auto found = players_.find(*resuming);
1252
+ if (found != players_.end() && found->second.peer) {
1253
+ auto old = found->second.peer;
1254
+ old->on_close = [] {};
1255
+ old->close(4000);
1256
+ if (host_) { Json left = msg("left"); left["player"] = *resuming; host_->send_text(left.dump()); }
1257
+ }
1258
+ }
1259
+ if (!number) {
1260
+ for (int n = 1; n <= wire::players; n++) {
1261
+ auto found = players_.find(n);
1262
+ if (found == players_.end() || (!found->second.peer && found->second.held_until < t)) { number = n; break; }
1263
+ }
1264
+ }
1265
+ if (!number) {
1266
+ peer->send_text(R"({"t":"full"})");
1267
+ return peer->close(4004);
1268
+ }
1269
+ int place = *number;
1270
+ players_.erase(place);
1271
+ auto& player = players_.emplace(place, Player(peer, t)).first->second;
1272
+ (void)player;
1273
+ Json welcome = msg("welcome");
1274
+ welcome["role"] = "controller";
1275
+ welcome["place"] = place;
1276
+ welcome["host"] = host_ != nullptr;
1277
+ welcome["credential"] = token::issue(token::Kind::controller, app_, key_, place, t / 1000);
1278
+ peer->send_text(welcome.dump());
1279
+ if (host_) { Json joined = msg("joined"); joined["player"] = place; host_->send_text(joined.dump()); }
1280
+ Peer* raw = peer.get();
1281
+ peer->on_text = [this, place, raw](std::string const& text) {
1282
+ auto found = players_.find(place);
1283
+ if (found != players_.end() && found->second.peer.get() == raw) from_player(place, found->second, text);
1284
+ };
1285
+ peer->on_binary = [this, place, raw](std::string const& data) {
1286
+ auto found = players_.find(place);
1287
+ if (found != players_.end() && found->second.peer.get() == raw) piece_from_player(place, found->second, data);
1288
+ };
1289
+ peer->on_close = [this, place, raw] {
1290
+ auto found = players_.find(place);
1291
+ if (found == players_.end() || found->second.peer.get() != raw) return;
1292
+ found->second.peer.reset();
1293
+ found->second.held_until = now() + token::lifetime_s(token::Kind::controller) * 1000;
1294
+ found->second.to_host.clear();
1295
+ found->second.to_phone.clear();
1296
+ if (host_) { Json left = msg("left"); left["player"] = place; host_->send_text(left.dump()); }
1297
+ };
1298
+ }
1299
+
1300
+ void refuse(Player& player, uint32_t id, std::string const& reason) {
1301
+ if (!player.peer) return;
1302
+ Json r = msg("file-refuse");
1303
+ r["id"] = (int64_t)id;
1304
+ r["reason"] = reason;
1305
+ player.peer->send_text(r.dump());
1306
+ }
1307
+
1308
+ void from_player(int number, Player& player, std::string const& text) {
1309
+ auto parsed = parse(text);
1310
+ if (!parsed) return;
1311
+ Json const& m = *parsed;
1312
+ std::string kind = m.str("t").value_or("");
1313
+ bool flow = kind == "file-ack" || kind == "file-cancel" || kind == "file-refuse";
1314
+ int64_t t = now();
1315
+ if (!flow && !player.rate.allowed(player.peer.get(), t)) return;
1316
+ if (!player.peer) return;
1317
+ if (!host_) {
1318
+ if (kind == "file") if (auto id = id_of(m)) refuse(player, *id, "The TV app isn't there.");
1319
+ return;
1320
+ }
1321
+ if (host_->pending_text() > host_behind && !flow && kind != "button") {
1322
+ if (kind == "file") if (auto id = id_of(m)) refuse(player, *id, "The TV app is busy; try again.");
1323
+ return;
1324
+ }
1325
+ auto seq = m.integer("seq").value_or(0);
1326
+ auto name = m.str("name");
1327
+ bool named = name && !name->empty() && name->size() <= 32
1328
+ && std::all_of(name->begin(), name->end(), [](char c) { return isalnum((unsigned char)c) || c == '_' || c == '-'; });
1329
+ if (kind == "button") {
1330
+ if (!named) return;
1331
+ Json out = msg("button");
1332
+ out["player"] = number; out["name"] = *name; out["down"] = m.is_true("down"); out["seq"] = seq; out["at"] = t;
1333
+ host_->send_text(out.dump());
1334
+ } else if (kind == "axis") {
1335
+ if (!named || host_->pending_text() > wire::backlog) return;
1336
+ auto clamp = [&](const char* key) {
1337
+ auto v = m.get(key);
1338
+ if (!v || v->type != Json::Type::number || !std::isfinite(v->n)) return 0.0;
1339
+ return std::max(-1.0, std::min(1.0, v->n));
1340
+ };
1341
+ Json out = msg("axis");
1342
+ out["player"] = number; out["name"] = *name; out["x"] = clamp("x"); out["y"] = clamp("y"); out["seq"] = seq; out["at"] = t;
1343
+ host_->send_text(out.dump());
1344
+ } else if (kind == "message") {
1345
+ auto data = m.get("data");
1346
+ if (!data) return;
1347
+ Json out = msg("message");
1348
+ out["player"] = number; out["data"] = *data; out["at"] = t;
1349
+ host_->send_text(out.dump());
1350
+ } else if (kind == "too-big") {
1351
+ auto bytes = m.integer("bytes");
1352
+ if (!bytes) return;
1353
+ Json out = msg("too-big");
1354
+ out["player"] = number; out["bytes"] = *bytes;
1355
+ host_->send_text(out.dump());
1356
+ } else if (kind == "file") {
1357
+ auto id = id_of(m);
1358
+ if (!id) return;
1359
+ auto size = m.integer("size");
1360
+ if (!size || *size < 0 || *size > wire::file) return refuse(player, *id, "The file is too big to send.");
1361
+ if (player.to_host.count(*id) || player.to_host.size() >= wire::transfers) return refuse(player, *id, "Another file is still on its way.");
1362
+ player.to_host.emplace(*id, Transfer(*size));
1363
+ Json out = msg("file");
1364
+ out["player"] = number; out["id"] = (int64_t)*id; out["name"] = shorter(m, "name", 200); out["type"] = shorter(m, "type", 100);
1365
+ out["size"] = *size; out["at"] = t;
1366
+ host_->send_text(out.dump());
1367
+ } else if (kind == "file-cancel") {
1368
+ auto id = id_of(m);
1369
+ if (!id || !player.to_host.erase(*id)) return;
1370
+ Json out = msg("file-cancel");
1371
+ out["player"] = number; out["id"] = (int64_t)*id;
1372
+ host_->send_text(out.dump());
1373
+ } else if (kind == "file-ack") {
1374
+ auto id = id_of(m);
1375
+ auto got = m.integer("got");
1376
+ if (!id || !got) return;
1377
+ auto found = player.to_phone.find(*id);
1378
+ if (found == player.to_phone.end() || !found->second.acknowledge(*got)) return;
1379
+ if (*got == found->second.size) player.to_phone.erase(found);
1380
+ Json out = msg("file-ack");
1381
+ out["player"] = number; out["id"] = (int64_t)*id; out["got"] = *got;
1382
+ host_->send_text(out.dump());
1383
+ } else if (kind == "file-refuse") {
1384
+ auto id = id_of(m);
1385
+ if (!id || !player.to_phone.erase(*id)) return;
1386
+ Json out = msg("file-refuse");
1387
+ out["player"] = number; out["id"] = (int64_t)*id; out["reason"] = shorter(m, "reason", 200);
1388
+ host_->send_text(out.dump());
1389
+ }
1390
+ }
1391
+
1392
+ void piece_from_player(int number, Player& player, std::string const& data) {
1393
+ auto head = chunk_header(data);
1394
+ if (!head) return;
1395
+ auto found = player.to_host.find(head->second);
1396
+ if (found == player.to_host.end()) return;
1397
+ Transfer& transfer = found->second;
1398
+ int64_t bytes = (int64_t)data.size() - (int64_t)wire::header;
1399
+ if (!host_ || bytes <= 0 || transfer.passed + bytes > transfer.size
1400
+ || transfer.passed + bytes - transfer.acknowledged > wire::window + (int64_t)wire::chunk
1401
+ || !host_->send_piece(readdressed(data, (uint8_t)number))) {
1402
+ uint32_t id = head->second;
1403
+ player.to_host.erase(found);
1404
+ if (host_) { Json out = msg("file-cancel"); out["player"] = number; out["id"] = (int64_t)id; host_->send_text(out.dump()); }
1405
+ return refuse(player, id, "The file didn't keep to its size.");
1406
+ }
1407
+ transfer.passed += bytes;
1408
+ transfer.unacknowledged += 1;
1409
+ }
1410
+
1411
+ void from_host(Json const& m) {
1412
+ int number = (int)m.integer("player").value_or(0);
1413
+ auto found = players_.find(number);
1414
+ Player* player = found == players_.end() ? nullptr : &found->second;
1415
+ std::string kind = m.str("t").value_or("");
1416
+ auto refuse_host = [&](uint32_t id, std::string const& reason) {
1417
+ if (!host_) return;
1418
+ Json out = msg("file-refuse");
1419
+ out["player"] = number; out["id"] = (int64_t)id; out["reason"] = reason;
1420
+ host_->send_text(out.dump());
1421
+ };
1422
+ if (kind == "link") {
1423
+ send_link();
1424
+ } else if (kind == "send") {
1425
+ auto data = m.get("data");
1426
+ if (!data) return;
1427
+ Json out = msg("message");
1428
+ out["data"] = *data;
1429
+ std::string text = out.dump();
1430
+ for (auto& [place, each] : players_) if ((number == 0 || number == place) && each.peer) each.peer->send_text(text);
1431
+ } else if (kind == "player") {
1432
+ auto shown = m.integer("number");
1433
+ if (!player || !player->peer || !shown || *shown <= 0 || *shown >= 1000) return;
1434
+ Json out = msg("player");
1435
+ out["number"] = *shown;
1436
+ player->peer->send_text(out.dump());
1437
+ } else if (kind == "file") {
1438
+ auto id = id_of(m);
1439
+ if (!id) return;
1440
+ auto size = m.integer("size");
1441
+ if (!player || !player->peer || !size || *size < 0 || *size > wire::file || player->to_phone.count(*id)
1442
+ || player->to_phone.size() >= wire::transfers) return refuse_host(*id, "The phone can't take it now.");
1443
+ player->to_phone.emplace(*id, Transfer(*size));
1444
+ Json out = msg("file");
1445
+ out["id"] = (int64_t)*id; out["name"] = shorter(m, "name", 200); out["type"] = shorter(m, "type", 100); out["size"] = *size;
1446
+ player->peer->send_text(out.dump());
1447
+ } else if (kind == "file-cancel") {
1448
+ auto id = id_of(m);
1449
+ if (!id || !player || !player->to_phone.erase(*id)) return;
1450
+ if (player->peer) { Json out = msg("file-cancel"); out["id"] = (int64_t)*id; player->peer->send_text(out.dump()); }
1451
+ } else if (kind == "file-ack") {
1452
+ auto id = id_of(m);
1453
+ auto got = m.integer("got");
1454
+ if (!id || !got || !player) return;
1455
+ auto transfer = player->to_host.find(*id);
1456
+ if (transfer == player->to_host.end() || !transfer->second.acknowledge(*got)) return;
1457
+ if (*got == transfer->second.size) player->to_host.erase(transfer);
1458
+ if (player->peer) { Json out = msg("file-ack"); out["id"] = (int64_t)*id; out["got"] = *got; player->peer->send_text(out.dump()); }
1459
+ } else if (kind == "file-refuse") {
1460
+ auto id = id_of(m);
1461
+ if (!id || !player || !player->to_host.erase(*id)) return;
1462
+ if (player->peer) { Json out = msg("file-refuse"); out["id"] = (int64_t)*id; out["reason"] = shorter(m, "reason", 200); player->peer->send_text(out.dump()); }
1463
+ }
1464
+ }
1465
+
1466
+ void piece_from_host(std::string const& data) {
1467
+ auto head = chunk_header(data);
1468
+ if (!head) return;
1469
+ int number = head->first;
1470
+ auto found = players_.find(number);
1471
+ if (found == players_.end()) return;
1472
+ Player& player = found->second;
1473
+ auto transfer = player.to_phone.find(head->second);
1474
+ if (transfer == player.to_phone.end()) return;
1475
+ int64_t bytes = (int64_t)data.size() - (int64_t)wire::header;
1476
+ if (!player.peer || bytes <= 0 || transfer->second.passed + bytes > transfer->second.size
1477
+ || transfer->second.passed + bytes - transfer->second.acknowledged > wire::window + (int64_t)wire::chunk
1478
+ || !player.peer->send_piece(readdressed(data, 0))) {
1479
+ uint32_t id = head->second;
1480
+ player.to_phone.erase(transfer);
1481
+ if (player.peer) { Json out = msg("file-cancel"); out["id"] = (int64_t)id; player.peer->send_text(out.dump()); }
1482
+ if (host_) {
1483
+ Json out = msg("file-refuse");
1484
+ out["player"] = number; out["id"] = (int64_t)id; out["reason"] = "The file didn't keep to its size.";
1485
+ host_->send_text(out.dump());
1486
+ }
1487
+ return;
1488
+ }
1489
+ transfer->second.passed += bytes;
1490
+ transfer->second.unacknowledged += 1;
1491
+ }
1492
+
1493
+ void drop_transfers() {
1494
+ for (auto& [n, player] : players_) { player.to_host.clear(); player.to_phone.clear(); }
1495
+ }
1496
+
1497
+ // A link for phones: the page's address with a fresh join credential after the #.
1498
+ void send_link() {
1499
+ if (!page_ || !host_) return;
1500
+ Json out = msg("link");
1501
+ out["url"] = *page_ + "#" + token::issue(token::Kind::join, app_, key_, std::nullopt, now() / 1000);
1502
+ host_->send_text(out.dump());
1503
+ }
1504
+ };
1505
+
1506
+ } // namespace lampc