@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,941 @@
1
+ // A page on Android: Lamp's Android app around the page (`GameAndroid` in Lamp's app, one app for phones, tablets and
2
+ // TVs), built by Gradle with the tools Lamp keeps on this computer, and put on lamp's own emulator or on a device of
3
+ // the person's that adb sees. The project is written in lamp's own folder for the app, never in the app's: what's
4
+ // built is Lamp's host and the page's files as lamp read them, so nothing in the app's folder runs in the build.
5
+ //
6
+ // The tools and Gradle's cache are the ones every Lamp on this computer shares (they're large, and the same for each).
7
+ // lamp's adb server, its key to the person's devices and its emulators are its own, on ports and in folders no other
8
+ // Lamp uses, as each Lamp's are: a device allows lamp once, and an emulator lamp started is lamp's to close.
9
+
10
+ import { spawn } from "node:child_process";
11
+ import { createHash, randomUUID } from "node:crypto";
12
+ import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
13
+ import net from "node:net";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { Problem, stateOf } from "./state.mjs";
18
+ import { resized, scaled, sizeOf, tile } from "./png.mjs";
19
+ import { nameOf, pageFiles } from "./share.mjs";
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url));
22
+ // Lamp's Android app for a page and the tools it's built with, as Lamp's own code writes them (scripts/cli-files.sh).
23
+ const HOST = join(HERE, "lamps", "android-host");
24
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
25
+ const readJSON = (file) => { try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; } };
26
+
27
+ // lamp's own ports: its adb server's, and for each emulator its console's (adb reaches it on the next one up) and
28
+ // the one it's controlled on. Clear of every Lamp's (`Edition.androidPorts`).
29
+ const PORTS = { server: 47915, phone: { console: 47918, control: 47916 }, tv: { console: 47920, control: 47922 } };
30
+
31
+ const EMULATORS = {
32
+ phone: { word: "android", name: "the Android emulator", words: ["android", "android-phone", "android-emulator", "emulator"] },
33
+ tv: { word: "android-tv", name: "the Android TV emulator", words: ["android-tv", "android-tv-emulator", "tv-emulator"] },
34
+ };
35
+
36
+ /** What a game's page is told of the device it's on (`GamePage.descriptor` for Android: nothing of its own to name). */
37
+ const DEVICE = `// Written by Lamp for the device this copy of the game is on (android); don't edit.\nexport default ${JSON.stringify({ keys: {} })};\n`;
38
+
39
+ let read;
40
+ /**
41
+ * Lamp's app's own, for the calls it makes (`useLamps`): the tools list Lamp's code holds, the folder the tools are in,
42
+ * its own folders for adb's home and its emulators, and the environment its tools run with. Null for lamp's own.
43
+ */
44
+ let lamps = null;
45
+
46
+ /** Has what follows use Lamp's app's tools list, folders and environment, as it hands them over. */
47
+ export function useLamps({ spec: given, tools, userHome, avdHome, env }) {
48
+ if (!given || typeof tools !== "string" || typeof userHome !== "string" || typeof avdHome !== "string") throw new Error("the call needs Lamp's tools list and folders");
49
+ lamps = { spec: given, tools, userHome, avdHome, env: env ?? {} };
50
+ }
51
+
52
+ /** What Lamp builds an Android app with, and where its host's files go. */
53
+ function spec() {
54
+ if (lamps) return lamps.spec;
55
+ if (process.platform !== "darwin") throw new Problem("lamp builds for Android on a Mac, so far");
56
+ read ??= readJSON(join(HOST, "tools.json"));
57
+ if (!read) throw new Problem("Lamp's Android app for a page isn't beside this copy of lamp: scripts/cli-files.sh writes it");
58
+ return read;
59
+ }
60
+
61
+ /** Whether this copy of lamp has what it needs to put a page on Android at all. */
62
+ export const knowsAndroid = () => process.platform === "darwin" && existsSync(join(HOST, "tools.json"));
63
+
64
+ /** lamp's own: adb's key to the person's devices, the emulators it made, and which of them it started. */
65
+ export const androidHome = () => process.env.LAMP_ANDROID_HOME || join(homedir(), ".lamp", "android");
66
+ /** The tools, one copy for every Lamp on this Mac (`Edition.sharedDeveloperFolder`). */
67
+ export const toolsFolder = () => process.env.LAMP_ANDROID_TOOLS || join(homedir(), "Library", "Developer", "Lamp", "Android");
68
+ /** Gradle's own cache, every dependency it fetched, shared the same way (`Edition.sharedCachesFolder`). */
69
+ const gradleCache = () => process.env.LAMP_GRADLE_HOME || join(homedir(), "Library", "Caches", "Lamp", "gradle");
70
+
71
+ function paths() {
72
+ const tools = lamps?.tools ?? toolsFolder(), sdk = join(tools, "sdk"), jdk = join(tools, "jdk"), gradleFolder = join(tools, `gradle-${spec().gradle}`);
73
+ const javaHome = join(jdk, "Contents", "Home");
74
+ return {
75
+ tools, sdk, jdk, javaHome, gradleFolder,
76
+ java: join(javaHome, "bin", "java"), gradle: join(gradleFolder, "bin", "gradle"),
77
+ adb: join(sdk, "platform-tools", "adb"), emulator: join(sdk, "emulator", "emulator"),
78
+ sdkmanager: join(sdk, "cmdline-tools", "latest", "bin", "sdkmanager"), avdmanager: join(sdk, "cmdline-tools", "latest", "bin", "avdmanager"),
79
+ userHome: lamps?.userHome ?? join(androidHome(), "home"), avdHome: lamps?.avdHome ?? join(androidHome(), "avd"),
80
+ };
81
+ }
82
+
83
+ /** Which of the tools are on this computer (`AndroidTools.has…`). */
84
+ export function toolsThere() {
85
+ const p = paths(), s = spec(), image = (name) => existsSync(join(p.sdk, ...name.split(";"), "system.img"));
86
+ const there = {
87
+ jdk: existsSync(p.java), commandLineTools: existsSync(p.sdkmanager), gradle: existsSync(p.gradle),
88
+ buildPackages: existsSync(p.adb) && existsSync(join(p.sdk, "build-tools", s.buildTools, "aapt2")) && existsSync(join(p.sdk, "platforms", `android-${s.platform}`, "android.jar")),
89
+ emulatorPackages: existsSync(p.emulator) && image(s.phone.image), tvImage: image(s.tv.image),
90
+ };
91
+ there.builds = there.jdk && there.commandLineTools && there.gradle && there.buildPackages;
92
+ return there;
93
+ }
94
+
95
+ const NO_TOOLS = (command) => `Android's tools aren't on this computer yet. "${command} devices add android" downloads them (about 2 GB, Google's and Gradle's own); it accepts Google's terms for them, so the person runs it in a terminal themselves.`;
96
+
97
+ /** What every command of Google's or Gradle's is run with (`AndroidTools.environment`), lamp's own server and folders in it. */
98
+ function environment() {
99
+ if (lamps) return { ...process.env, ...lamps.env };
100
+ const p = paths(), env = { ...process.env };
101
+ // A serial the person set for their own adb would send every command of lamp's to it.
102
+ delete env.ANDROID_SERIAL;
103
+ return {
104
+ ...env, JAVA_HOME: p.javaHome, ANDROID_HOME: p.sdk, ANDROID_SDK_ROOT: p.sdk, ANDROID_USER_HOME: p.userHome, ANDROID_AVD_HOME: p.avdHome,
105
+ GRADLE_USER_HOME: gradleCache(), ANDROID_ADB_SERVER_PORT: String(PORTS.server),
106
+ // lamp's server doesn't look for emulators on adb's usual ports: those are the person's, or another Lamp's.
107
+ ADB_LOCAL_TRANSPORT_MAX_PORT: "5554",
108
+ };
109
+ }
110
+
111
+ /** Runs a tool to its end: what it said, and whether it ended well. `binary` keeps its output as bytes. */
112
+ function run(file, args, { cwd, input, timeout = 60_000, env, binary = false } = {}) {
113
+ return new Promise((done) => {
114
+ const out = [], err = [];
115
+ let timedOut = false, child;
116
+ const end = (code, failure) => {
117
+ clearTimeout(timer);
118
+ const all = Buffer.concat(out);
119
+ done({ ok: code === 0 && !timedOut, code, out: binary ? all : all.toString("utf8"), err: failure ?? Buffer.concat(err).toString("utf8"), timedOut });
120
+ };
121
+ try { child = spawn(file, args, { cwd, env: env ?? environment(), stdio: ["pipe", "pipe", "pipe"] }); }
122
+ catch (error) { return done({ ok: false, code: null, out: binary ? Buffer.alloc(0) : "", err: String(error.message), timedOut: false }); }
123
+ const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, timeout);
124
+ child.stdout.on("data", (chunk) => out.push(chunk));
125
+ child.stderr.on("data", (chunk) => err.push(chunk));
126
+ child.on("error", (error) => end(null, String(error.message)));
127
+ child.on("close", (code) => end(code));
128
+ child.stdin.on("error", () => {});
129
+ child.stdin.end(input ?? "");
130
+ });
131
+ }
132
+
133
+ const tail = (text, lines) => text.split(/\r?\n/).filter((line) => line.trim()).slice(-lines).join("\n");
134
+
135
+ /** Runs a tool to its end as `run` does, telling `heard` each line it prints as it prints it. */
136
+ function streamed(file, args, { cwd, env, timeout = 60_000 } = {}, heard = () => {}) {
137
+ return new Promise((done) => {
138
+ const out = [];
139
+ let waiting = "", timedOut = false;
140
+ const child = spawn(file, args, { cwd, env: env ?? environment(), stdio: ["ignore", "pipe", "pipe"] });
141
+ const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, timeout);
142
+ const hear = (chunk) => {
143
+ out.push(chunk);
144
+ const lines = (waiting + chunk.toString("utf8")).split(/[\r\n]/);
145
+ waiting = lines.pop();
146
+ for (const line of lines) if (line.trim()) heard(line);
147
+ };
148
+ child.stdout.on("data", hear);
149
+ child.stderr.on("data", hear);
150
+ child.on("error", () => { clearTimeout(timer); done({ ok: false, out: Buffer.concat(out).toString("utf8") }); });
151
+ child.on("close", (code) => { clearTimeout(timer); done({ ok: code === 0 && !timedOut, out: Buffer.concat(out).toString("utf8") }); });
152
+ });
153
+ }
154
+
155
+ // MARK: adb
156
+
157
+ let server;
158
+ /** lamp's adb server, started once for this command with nothing of this command's held open by it. */
159
+ function serverStarted() {
160
+ server ??= new Promise((done) => {
161
+ const p = paths();
162
+ mkdirSync(p.userHome, { recursive: true });
163
+ const child = spawn(p.adb, ["start-server"], { env: environment(), stdio: "ignore" });
164
+ const timer = setTimeout(() => { child.kill(); done(); }, 30_000);
165
+ child.on("error", () => { clearTimeout(timer); done(); });
166
+ child.on("exit", () => { clearTimeout(timer); done(); });
167
+ });
168
+ return server;
169
+ }
170
+
171
+ async function adb(args, { serial, timeout = 60_000, input, binary } = {}) {
172
+ await serverStarted();
173
+ return run(paths().adb, [...(serial ? ["-s", serial] : []), ...args], { timeout, input, binary });
174
+ }
175
+
176
+ /**
177
+ * Ends lamp's adb server as a command of lamp's ends, if this command started or used it and none of lamp's emulators
178
+ * is running: a server left behind would go on holding the person's plugged-in devices, which their own adb then
179
+ * couldn't use. While an emulator runs it stays, so each look at it is quick; the next command starts one otherwise.
180
+ */
181
+ export async function androidDone() {
182
+ if (!server) return;
183
+ await server;
184
+ for (const kind of Object.keys(EMULATORS)) if (await answers(PORTS[kind].console)) return;
185
+ await run(paths().adb, ["kill-server"], { timeout: 10_000 });
186
+ }
187
+
188
+ /** Each line of `adb devices -l`: the selector adb takes with -s, its state, and the model adb read. */
189
+ async function listed() {
190
+ const said = (await adb(["devices", "-l"], { timeout: 30_000 })).out;
191
+ return said.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("*") && !line.startsWith("List of"))
192
+ .map((line) => line.split(/\s+/)).filter((parts) => parts.length >= 2)
193
+ .map(([serial, state, ...facts]) => ({ serial, state, model: facts.find((fact) => fact.startsWith("model:"))?.slice(6).replaceAll("_", " ") }));
194
+ }
195
+
196
+ /** What the person does about a device adb lists in a state it can't be used in (`Adb.advice`). */
197
+ function advice(name, state) {
198
+ if (state === "unauthorized") return `On ${name}, choose Allow when it asks whether to allow debugging from this computer (tick Always allow).`;
199
+ if (state === "offline") return `${name} isn't answering. Unplug it and plug it in again, and unlock it.`;
200
+ return `On ${name}, open Settings → About phone and tap Build number seven times to turn on Developer options, then in Settings → System → Developer options turn on USB debugging, and plug it in.`;
201
+ }
202
+
203
+ /** The person's own devices adb sees, each with its name and whether lamp is allowed on it. lamp's emulators aren't among them. */
204
+ export async function androidDevices() {
205
+ const emulators = new Set(Object.keys(EMULATORS).map(serialOf)), found = [];
206
+ for (const device of await listed()) {
207
+ if (emulators.has(device.serial)) continue;
208
+ let name = device.model ?? device.serial, id = device.serial, tv = false;
209
+ if (device.state === "device") {
210
+ const keys = ["ro.product.manufacturer", "ro.product.model", "ro.serialno"];
211
+ const asked = keys.map((key) => `echo ${key}=$(getprop ${key})`).join(";") + ";echo leanback=$(pm has-feature android.software.leanback)";
212
+ const said = Object.fromEntries((await adb(["shell", asked], { serial: device.serial, timeout: 20_000 })).out.split("\n")
213
+ .map((line) => line.trim()).filter((line) => line.includes("=")).map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]));
214
+ const maker = said["ro.product.manufacturer"] ?? "", model = said["ro.product.model"] || name;
215
+ name = !maker || model.toLowerCase().startsWith(maker.toLowerCase()) ? model : `${maker} ${model}`;
216
+ // The serial the device reads is its own; adb's selector for one on the network is only where it is now.
217
+ if (said["ro.serialno"]) id = said["ro.serialno"];
218
+ tv = said.leanback === "true";
219
+ }
220
+ found.push({ serial: device.serial, state: device.state, name, id, tv });
221
+ }
222
+ return found;
223
+ }
224
+
225
+ // MARK: The emulators
226
+
227
+ const serialOf = (kind) => `emulator-${PORTS[kind].console}`;
228
+ const recordOf = (kind) => join(androidHome(), `emulator-${kind}.json`);
229
+
230
+ /** Whether something answers on one of this computer's ports. */
231
+ function answers(port) {
232
+ return new Promise((done) => {
233
+ const socket = net.connect(port, "127.0.0.1");
234
+ const end = (there) => { socket.destroy(); done(there); };
235
+ socket.setTimeout(1500, () => end(false));
236
+ socket.on("connect", () => end(true));
237
+ socket.on("error", () => end(false));
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Tells lamp's adb server about lamp's emulator on its ports, in the message an emulator sends it as it boots (adb's
243
+ * `host:emulator:<port>`, its length first in hex): a server started after the emulator hasn't heard of it.
244
+ */
245
+ function register(kind) {
246
+ return new Promise((done) => {
247
+ const request = `host:emulator:${PORTS[kind].console + 1}`, socket = net.connect(PORTS.server, "127.0.0.1");
248
+ socket.setTimeout(2000, () => { socket.destroy(); done(false); });
249
+ socket.on("connect", () => socket.end(request.length.toString(16).padStart(4, "0") + request, () => done(true)));
250
+ socket.on("error", () => done(false));
251
+ });
252
+ }
253
+
254
+ /** Whether lamp's emulator of that kind is running and adb reaches it. Another emulator adb lists isn't lamp's to use. */
255
+ async function emulatorRunning(kind) {
256
+ const serial = serialOf(kind), there = async () => (await listed()).some((device) => device.serial === serial && device.state === "device");
257
+ if (await there()) return true;
258
+ if (!(await answers(PORTS[kind].console))) return false;
259
+ await register(kind);
260
+ await sleep(500);
261
+ return there();
262
+ }
263
+
264
+ /** Makes one of lamp's emulators from its system image: a recent Pixel, or a 1080p TV (`Emulator.create`). */
265
+ export async function makeEmulator(kind) {
266
+ const p = paths(), s = spec()[kind];
267
+ mkdirSync(p.avdHome, { recursive: true });
268
+ mkdirSync(p.userHome, { recursive: true });
269
+ // avdmanager asks whether to make a hardware profile of its own; the answer is no.
270
+ const made = await run(p.avdmanager, ["create", "avd", "-n", s.avd, "-k", s.image, "-d", s.profile, "--force"], { input: "no\n", timeout: 120_000 });
271
+ if (!made.ok || !existsSync(join(p.avdHome, `${s.avd}.avd`, "config.ini"))) throw new Problem(`lamp couldn't make the emulator.\n${tail(made.out + made.err, 8)}`);
272
+ }
273
+
274
+ /**
275
+ * Starts lamp's emulator of that kind when it isn't running, waits for Android to boot, and gives back its serial. It's
276
+ * a window of its own on the Mac; `hidden` starts it with none and no sound, for a check nobody watches. One running
277
+ * hidden is opened again with its window when the person asks to see it.
278
+ */
279
+ /** A Problem when that emulator isn't downloaded, saying how it is. */
280
+ function emulatorThere(kind, command) {
281
+ const there = toolsThere();
282
+ if (!there.builds) throw new Problem(NO_TOOLS(command));
283
+ if (there.emulatorPackages && (kind !== "tv" || there.tvImage)) return;
284
+ const word = EMULATORS[kind].word;
285
+ throw new Problem(`${EMULATORS[kind].name[0].toUpperCase()}${EMULATORS[kind].name.slice(1)} isn't on this computer yet. "${command} devices add ${word}" downloads it (about a gigabyte); it accepts Google's terms for it, so the person runs it in a terminal themselves.`);
286
+ }
287
+
288
+ async function startEmulator(kind, { hidden = false, said = () => {}, command = "lamp" } = {}) {
289
+ const p = paths(), s = spec()[kind], serial = serialOf(kind);
290
+ emulatorThere(kind, command);
291
+ if (await emulatorRunning(kind)) {
292
+ if (hidden || !readJSON(recordOf(kind))?.hidden) return serial;
293
+ said(`Opening ${EMULATORS[kind].name} again with its window…`);
294
+ await closeEmulator(kind);
295
+ }
296
+ if (!existsSync(join(p.avdHome, `${s.avd}.avd`, "config.ini"))) {
297
+ said(`Making ${EMULATORS[kind].name}…`);
298
+ await makeEmulator(kind);
299
+ }
300
+ said(`Starting ${EMULATORS[kind].name}${hidden ? ", with no window" : ""}…`);
301
+ const { console: port, control } = PORTS[kind], logFile = join(androidHome(), `emulator-${kind}.log`);
302
+ // After a crash the emulator would otherwise open with a dialog asking to send Google a report, and start nothing
303
+ // until it's answered.
304
+ const args = ["-avd", s.avd, "-ports", `${port},${port + 1}`, "-grpc", String(control), "-crash-report-mode", "never", "-no-boot-anim",
305
+ "-netdelay", "none", "-netspeed", "full", ...(hidden ? ["-no-window", "-no-audio"] : [])];
306
+ const log = openSync(logFile, "w");
307
+ const child = spawn(p.emulator, args, { env: environment(), detached: true, stdio: ["ignore", log, log] });
308
+ let quit = false;
309
+ child.on("exit", () => { quit = true; });
310
+ child.on("error", () => { quit = true; });
311
+ child.unref();
312
+ writeFileSync(recordOf(kind), JSON.stringify({ hidden, pid: child.pid }));
313
+ // Booting takes most of a minute the first time, a few seconds from its saved state after that.
314
+ for (let waited = 0; waited < 240; waited++) {
315
+ await sleep(1000);
316
+ if (quit) { rmSync(recordOf(kind), { force: true }); throw new Problem(`The emulator quit while starting; what it said is in ${logFile}`); }
317
+ if ((await emulatorRunning(kind)) && (await adb(["shell", "getprop", "sys.boot_completed"], { serial, timeout: 10_000 })).out.trim() === "1") {
318
+ // Android covers the first full-screen app with a tip on leaving full screen; in lamp's own emulator it's taken as read.
319
+ await adb(["shell", "settings", "put", "secure", "immersive_mode_confirmations", "confirmed"], { serial, timeout: 10_000 });
320
+ return serial;
321
+ }
322
+ }
323
+ throw new Problem(`The emulator didn't finish starting; what it said is in ${logFile}`);
324
+ }
325
+
326
+ /** Closes lamp's emulator of that kind, saving where it is; false when it wasn't running. */
327
+ export async function closeEmulator(kind) {
328
+ if (!(await emulatorRunning(kind))) { rmSync(recordOf(kind), { force: true }); return false; }
329
+ await adb(["emu", "kill"], { serial: serialOf(kind), timeout: 20_000 });
330
+ for (let waited = 0; waited < 60 && (await answers(PORTS[kind].console)); waited++) await sleep(500);
331
+ rmSync(recordOf(kind), { force: true });
332
+ return true;
333
+ }
334
+
335
+ // MARK: Where a page goes
336
+
337
+ const emulatorPlace = (kind) => ({ name: EMULATORS[kind].name, word: EMULATORS[kind].word, key: `android:emulator-${kind}`, emulator: kind });
338
+
339
+ /** lamp's emulator a word names ("android", "android-tv"), or null. Nothing is started or asked of adb. */
340
+ export function emulatorNamed(word) {
341
+ const wanted = String(word).toLowerCase(), kind = Object.keys(EMULATORS).find((each) => EMULATORS[each].words.includes(wanted));
342
+ return kind ? emulatorPlace(kind) : null;
343
+ }
344
+
345
+ /** The person's own Android device a word names, by its name or adb's serial; null when adb sees none like it. */
346
+ export async function androidDeviceNamed(word) {
347
+ if (!knowsAndroid() || !toolsThere().buildPackages) return null;
348
+ const wanted = String(word).toLowerCase(), all = await androidDevices();
349
+ const exact = all.filter((device) => device.serial.toLowerCase() === wanted || device.id.toLowerCase() === wanted || device.name.toLowerCase() === wanted);
350
+ const found = exact.length ? exact : all.filter((device) => device.name.toLowerCase().includes(wanted));
351
+ if (!found.length) return null;
352
+ if (found.length > 1) throw new Problem(`"${word}" could be ${found.map((device) => `"${device.name}" (${device.serial})`).join(" or ")}: say which by its serial.`);
353
+ if (found[0].state !== "device") throw new Problem(advice(found[0].name, found[0].state));
354
+ return { name: found[0].name, word: found[0].serial, key: `android:${found[0].id}`, serial: found[0].serial };
355
+ }
356
+
357
+ /** The serial adb reaches the place by: an emulator's once it runs (started when `start` says so, else null), a device's own. */
358
+ async function reach(place, { start = false, hidden, said, command } = {}) {
359
+ if (!place.emulator) return place.serial;
360
+ if (start) return startEmulator(place.emulator, { hidden, said, command });
361
+ return (await emulatorRunning(place.emulator)) ? serialOf(place.emulator) : null;
362
+ }
363
+
364
+ /** adb's own word that it couldn't reach the device, which isn't the app's doing. */
365
+ function away(place, result) {
366
+ const said = (result.out + result.err).toLowerCase();
367
+ return result.timedOut || /not found|offline|no devices|unauthorized|closed/.test(said) ? new Problem(`${place.name} isn't answering. ${place.emulator ? "Open the page on it again." : "Plug it in again and unlock it."}`) : null;
368
+ }
369
+
370
+ // MARK: The app
371
+
372
+ /** The app's name to lamp, which its package is named for: its folder's own, so two apps never share a package. */
373
+ const appID = (layout) => stateOf(layout.folder).split(/[\\/]/).pop();
374
+
375
+ /**
376
+ * The app's package name, which Android ties its data and identity to: lamp's own prefix, so a copy lamp puts on a
377
+ * device never takes the place of one Lamp's app put there, and the app's id with what a Java name can't carry made safe.
378
+ */
379
+ export const packageOf = (layout) => `dev.lamp.cli.${appID(layout).replace(/[^A-Za-z0-9]/g, "_").replace(/^(?=\d)/, "_").toLowerCase()}`;
380
+
381
+ /** The app's name as its manifest's label: text, never markup or a reference to a resource. */
382
+ const label = (name) => name.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/^([@?])/, "\\$1");
383
+
384
+ function iconOf(layout) {
385
+ for (const file of [join(layout.folder, ".lamp", "AppIcon-square.png"), ...["apple-touch-icon.png", "icon.png", "favicon.png"].map((name) => join(layout.root, name))]) {
386
+ try { const bytes = readFileSync(file); if (sizeOf(bytes)) return bytes; } catch {}
387
+ }
388
+ return null;
389
+ }
390
+
391
+ /** Writes a file only when it isn't that already, so a build finds nothing changed where nothing is. */
392
+ function put(file, content) {
393
+ const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content);
394
+ try { if (readFileSync(file).equals(bytes)) return; } catch {}
395
+ try {
396
+ mkdirSync(dirname(file), { recursive: true });
397
+ writeFileSync(file, bytes);
398
+ } catch {
399
+ // A folder where the file goes, or a file where its folder does, from the page as it was before.
400
+ rmSync(file, { recursive: true, force: true });
401
+ mkdirSync(dirname(file), { recursive: true });
402
+ writeFileSync(file, bytes);
403
+ }
404
+ }
405
+
406
+ /** Makes a folder hold exactly these files: what's gone from the page is gone from its copy. */
407
+ function sync(folder, files) {
408
+ const kept = new Set(files.map(([path]) => path));
409
+ const clear = (at, prefix) => {
410
+ for (const entry of readdirSync(at, { withFileTypes: true })) {
411
+ const path = prefix + entry.name, file = join(at, entry.name);
412
+ if (!entry.isDirectory()) { if (!kept.has(path)) rmSync(file, { force: true }); continue; }
413
+ clear(file, path + "/");
414
+ if (!readdirSync(file).length) rmSync(file, { recursive: true, force: true });
415
+ }
416
+ };
417
+ if (existsSync(folder)) clear(folder, "");
418
+ for (const [path, bytes] of files) put(join(folder, ...path.split("/")), bytes);
419
+ }
420
+
421
+ const buildFolder = (layout) => join(stateOf(layout.folder), "android");
422
+ const recordFolder = (layout, place) => join(stateOf(layout.folder), "device", createHash("sha256").update(place.key).digest("hex").slice(0, 8));
423
+
424
+ /**
425
+ * Writes Lamp's Android project for the page in lamp's folder (`GameAndroid.write`, with the page and the icon where
426
+ * build.sh puts them) and builds it: the same app for every Android device.
427
+ */
428
+ async function build(layout, { command = "lamp" } = {}) {
429
+ const s = spec(), p = paths();
430
+ if (!toolsThere().builds) throw new Problem(NO_TOOLS(command));
431
+ const root = buildFolder(layout), project = join(root, ...s.folder.split("/"));
432
+ const id = appID(layout), pkg = packageOf(layout), kind = layout.game ? "game" : "page", icon = iconOf(layout);
433
+ const files = pageFiles(layout, { device: DEVICE });
434
+ const fill = (text) => text.replaceAll("{{PACKAGE}}", pkg).replaceAll("{{ANDROID_PACKAGE}}", pkg).replaceAll("{{SLUG}}", id)
435
+ .replaceAll("{{EXE}}", pkg.split(".").pop()).replaceAll("{{NAME}}", label(nameOf(layout)));
436
+ const host = (path) => fill(readFileSync(join(HOST, ...path.split("/")), "utf8"));
437
+ const kotlin = `app/src/main/kotlin/${pkg.replaceAll(".", "/")}`;
438
+ const written = [
439
+ ...["settings.gradle.kts", "build.gradle.kts", "gradle.properties", "gradle/libs.versions.toml", "app/build.gradle.kts"].map((path) => [path, host(path)]),
440
+ ["app/src/main/AndroidManifest.xml", host(`${kind}/AndroidManifest.xml`)],
441
+ [`${kotlin}/MainActivity.kt`, host(`${kind}/MainActivity.kt`)],
442
+ [`${kotlin}/LampPageGateway.kt`, host("LampPageGateway.kt")],
443
+ // The banner a TV's home screen shows: the app's icon on a dark tile, or the tile alone until it has one.
444
+ [s.banner, tile(icon, 320, 180)],
445
+ ["local.properties", `sdk.dir=${p.sdk}\n`],
446
+ ];
447
+ for (const [path, content] of written) put(join(project, ...path.split("/")), content);
448
+ sync(join(root, "build", "android-assets", "Web"), files);
449
+ put(join(root, "build", "android-res", "mipmap-xxxhdpi", "ic_launcher.png"), scaled(icon, 432) ?? tile(null, 432, 432));
450
+ mkdirSync(gradleCache(), { recursive: true });
451
+ mkdirSync(p.userHome, { recursive: true });
452
+ const ran = await run(p.gradle, ["--console=plain", "-q", `-Dlamp.app=${id}`, "assembleDebug"], { cwd: project, timeout: 20 * 60_000 });
453
+ const apk = join(project, ...s.apk.split("/"));
454
+ if (!ran.ok || !existsSync(apk)) throw new Problem(`The Android build didn't finish${ran.timedOut ? " in time" : ""}:\n${tail(ran.out + ran.err, 40)}`);
455
+ return { apk, package: pkg, files: files.length, game: layout.game };
456
+ }
457
+
458
+ /** The app's process on the device, or null when it isn't running. */
459
+ async function pidOf(pkg, serial) {
460
+ const pid = Number((await adb(["shell", "pidof", pkg], { serial, timeout: 20_000 })).out.trim().split(/\s+/)[0]);
461
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
462
+ }
463
+
464
+ /**
465
+ * Whether the app is the one in front, which is where a tap or a key goes: Android keeps an app's process after Home is
466
+ * pressed or another app opens, so its process alone says nothing of it.
467
+ */
468
+ export async function inFront(pkg, serial) {
469
+ const said = (await adb(["shell", "dumpsys", "activity", "activities"], { serial, timeout: 20_000 })).out;
470
+ const front = said.split("\n").filter((line) => /ResumedActivity\b/.test(line));
471
+ return front.some((line) => line.includes(` ${pkg}/`));
472
+ }
473
+
474
+ /**
475
+ * Builds the page's Android app and puts it on the place in the stead of the copy there, then opens it. A copy signed
476
+ * with another key can't be updated: it's taken off first, and what it saved with it, since Android allows no other way.
477
+ */
478
+ export async function putOnAndroid(layout, place, { said = () => {}, hidden = false, command = "lamp" } = {}) {
479
+ const name = nameOf(layout);
480
+ // Said before anything is built: an emulator that isn't downloaded can't be opened in.
481
+ if (place.emulator) emulatorThere(place.emulator, command);
482
+ said(`Building ${name} for Android…`);
483
+ const built = await build(layout, { command });
484
+ const serial = await reach(place, { start: true, hidden, said, command });
485
+ said(`Installing it on ${place.name}…`);
486
+ let installed = await adb(["install", "-r", "-t", built.apk], { serial, timeout: 300_000 });
487
+ if (!installed.ok && (installed.out + installed.err).includes("INSTALL_FAILED_UPDATE_INCOMPATIBLE")) {
488
+ await adb(["uninstall", built.package], { serial, timeout: 60_000 });
489
+ installed = await adb(["install", "-r", "-t", built.apk], { serial, timeout: 300_000 });
490
+ }
491
+ if (!installed.ok) throw away(place, installed) ?? new Problem(`Couldn't put ${name} on ${place.name}: ${tail(installed.out + installed.err, 6)}`);
492
+ said("Opening it…");
493
+ // The device's own clock as the app starts: its log is read from then on.
494
+ const since = (await adb(["shell", "date", "+%s"], { serial, timeout: 10_000 })).out.trim();
495
+ mkdirSync(recordFolder(layout, place), { recursive: true });
496
+ writeFileSync(join(recordFolder(layout, place), "installed.json"), JSON.stringify({ package: built.package, since: /^\d+$/.test(since) ? since : null }));
497
+ // lamp's own emulator may have put its screen to sleep; a device of the person's is theirs to wake.
498
+ if (place.emulator) await adb(["shell", "input keyevent KEYCODE_WAKEUP; wm dismiss-keyguard"], { serial, timeout: 20_000 });
499
+ await adb(["shell", "am", "force-stop", built.package], { serial, timeout: 30_000 });
500
+ const started = await adb(["shell", "am", "start", "-W", "-n", `${built.package}/.MainActivity`], { serial, timeout: 60_000 });
501
+ if (!started.ok || started.out.includes("Error")) throw away(place, started) ?? new Problem(`${name} didn't open on ${place.name}: ${tail(started.out + started.err, 6)}`);
502
+ let pid = null;
503
+ for (let tries = 0; tries < 20 && !(pid = await pidOf(built.package, serial)); tries++) await sleep(250);
504
+ if (!pid) throw new Problem(`${name} opened on ${place.name} and closed at once: ${command} look --on ${place.word} shows what it said.`);
505
+ return { name, package: built.package, files: built.files, game: built.game, line: `${name} is open on ${place.name}: ${built.files} files${hidden && place.emulator ? ", in an emulator with no window" : ""}.` };
506
+ }
507
+
508
+ const installedOn = (layout, place) => readJSON(join(recordFolder(layout, place), "installed.json"));
509
+
510
+ /** What the page said since it was opened: its console, and how the app ended if Android ended it. */
511
+ async function logOf(mine, serial, pid) {
512
+ const said = await adb(["logcat", "-d", "-v", "brief", ...(mine.since ? ["-T", `${mine.since}.000`] : []), ...(pid ? [`--pid=${pid}`] : []), "-s", "Lamp:V", "AndroidRuntime:E"], { serial, timeout: 30_000 });
513
+ if (!said.ok) return null;
514
+ return said.out.split(/\r?\n/).map((line) => /^[VDIWEF]\/([^\s(]+)\s*\(\s*\d+\):\s?(.*)$/.exec(line)).filter(Boolean)
515
+ .map(([, tag, text]) => (tag === "Lamp" ? text : `${tag}: ${text}`)).join("\n");
516
+ }
517
+
518
+ /** The place's screen now, kept as a picture in lamp's folder, with whether the app is open there and what its page said. */
519
+ export async function lookOnAndroid(layout, place, { command = "lamp" } = {}) {
520
+ const serial = await reach(place);
521
+ if (!serial) throw new Problem(`${place.name} isn't running: ${command} open --on ${place.word} starts it, with the page in it.`);
522
+ const mine = installedOn(layout, place), seen = { device: place.name, installed: !!mine, running: null, picture: null, log: null };
523
+ const pid = mine ? await pidOf(mine.package, serial) : null;
524
+ if (mine) seen.running = pid !== null;
525
+ const shot = await adb(["exec-out", "screencap", "-p"], { serial, timeout: 60_000, binary: true });
526
+ if (!shot.ok) { const gone = away(place, { ...shot, out: "" }); if (gone) throw gone; }
527
+ if (shot.ok && sizeOf(shot.out)) {
528
+ seen.picture = join(recordFolder(layout, place), "look.png");
529
+ mkdirSync(dirname(seen.picture), { recursive: true });
530
+ writeFileSync(seen.picture, shot.out);
531
+ seen.size = sizeOf(shot.out);
532
+ }
533
+ if (mine) seen.log = await logOf(mine, serial, pid);
534
+ return seen;
535
+ }
536
+
537
+ /** An emulator lamp started with no window is closed once its app is: nobody can see it to close it themselves. */
538
+ async function closeIfHidden(place) {
539
+ if (!place.emulator || !readJSON(recordOf(place.emulator))?.hidden) return false;
540
+ return closeEmulator(place.emulator);
541
+ }
542
+
543
+ /** Ends the app on the place; false when lamp never put it there. */
544
+ export async function stopOnAndroid(layout, place) {
545
+ const mine = installedOn(layout, place);
546
+ if (!mine) return false;
547
+ const serial = await reach(place);
548
+ if (!serial) return true;
549
+ const stopped = await adb(["shell", "am", "force-stop", mine.package], { serial, timeout: 30_000 });
550
+ if (!stopped.ok) throw away(place, stopped) ?? new Problem(`Couldn't stop it on ${place.name}: ${tail(stopped.out + stopped.err, 4)}`);
551
+ await closeIfHidden(place);
552
+ return true;
553
+ }
554
+
555
+ /**
556
+ * Takes the app off the place, and what it saved there with it; false when lamp never put it there. An emulator that
557
+ * isn't running is started for it with no window, and closed again after.
558
+ */
559
+ export async function removeFromAndroid(layout, place, { command = "lamp" } = {}) {
560
+ const mine = installedOn(layout, place);
561
+ if (!mine) return false;
562
+ const serial = await reach(place, { start: true, hidden: true, command });
563
+ const removed = await adb(["uninstall", mine.package], { serial, timeout: 60_000 });
564
+ const gone = away(place, removed);
565
+ if (gone) throw gone;
566
+ rmSync(join(recordFolder(layout, place), "installed.json"), { force: true });
567
+ await closeIfHidden(place);
568
+ return true;
569
+ }
570
+
571
+ // MARK: Trying it
572
+
573
+ // A key's name as lamp use takes it, as Android's.
574
+ const KEYS = { space: "SPACE", enter: "ENTER", return: "ENTER", escape: "ESCAPE", tab: "TAB", backspace: "DEL", delete: "FORWARD_DEL", up: "DPAD_UP", down: "DPAD_DOWN",
575
+ left: "DPAD_LEFT", right: "DPAD_RIGHT", shift: "SHIFT_LEFT", control: "CTRL_LEFT", alt: "ALT_LEFT", command: "META_LEFT", back: "BACK", home: "HOME", menu: "MENU",
576
+ select: "DPAD_CENTER", ok: "DPAD_CENTER", play: "MEDIA_PLAY_PAUSE" };
577
+ // A controller's button as lamp use takes it, as Android's.
578
+ const BUTTONS = { a: "BUTTON_A", b: "BUTTON_B", x: "BUTTON_X", y: "BUTTON_Y", lb: "BUTTON_L1", rb: "BUTTON_R1", menu: "BUTTON_START", view: "BUTTON_SELECT",
579
+ up: "DPAD_UP", down: "DPAD_DOWN", left: "DPAD_LEFT", right: "DPAD_RIGHT", ls: "BUTTON_THUMBL", rs: "BUTTON_THUMBR" };
580
+
581
+ /** One move as the device's own `input` command; a Problem for one it can't make. */
582
+ function inputFor(move) {
583
+ const number = (value, what) => { const n = Number(value); if (!Number.isFinite(n)) throw new Problem(`${move.kind} takes ${what}`); return Math.round(n); };
584
+ const held = (seconds) => (Number(seconds) > 0 ? ["--duration", String(Math.round(Math.min(Number(seconds), 30) * 1000))] : []);
585
+ switch (move.kind) {
586
+ case "click":
587
+ if (move.target !== undefined) throw new Problem("on a device, click takes a point of the picture: click <x> <y>");
588
+ return ["tap", String(number(move.x, "a point: click <x> <y>")), String(number(move.y, "a point: click <x> <y>"))];
589
+ case "drag":
590
+ return ["swipe", ...[move.x, move.y, move.toX, move.toY].map((value) => String(number(value, "two points: drag <x> <y> <to x> <to y>"))), "400"];
591
+ case "type":
592
+ // `input text` takes a space as %s and hands the rest to the device's shell: quoted whole.
593
+ return ["text", `'${String(move.text).replaceAll("'", "'\\''").replaceAll(" ", "%s")}'`];
594
+ case "key": {
595
+ const names = String(move.key).toLowerCase().split("+");
596
+ if (names.length > 1) throw new Problem("on a device, keys are pressed one at a time");
597
+ const code = KEYS[names[0]] ?? (/^[a-z0-9]$/.test(names[0]) ? names[0].toUpperCase() : /^f([1-9]|1[0-2])$/.test(names[0]) ? names[0].toUpperCase() : null);
598
+ if (!code) throw new Problem(`lamp doesn't know a key called "${move.key}" on Android`);
599
+ return ["keyevent", ...held(move.seconds), `KEYCODE_${code}`];
600
+ }
601
+ case "button": {
602
+ const names = String(move.buttons).toLowerCase().split("+");
603
+ if (names.length > 1) throw new Problem("on a device, a controller's buttons are pressed one at a time");
604
+ if (!BUTTONS[names[0]]) throw new Problem(`lamp can't press "${move.buttons}" on Android: it presses ${Object.keys(BUTTONS).join(", ")}`);
605
+ return ["gamepad", "keyevent", ...held(move.seconds), `KEYCODE_${BUTTONS[names[0]]}`];
606
+ }
607
+ case "wait": return null;
608
+ default: throw new Problem(`lamp can't ${move.kind === "stick" ? "move a stick" : move.kind} on a device: it clicks a point, drags, types, presses a key or a controller's button, and waits`);
609
+ }
610
+ }
611
+
612
+ /** Tries the app on the place as a person would, one move after another, then looks. */
613
+ export async function useOnAndroid(layout, place, moves, { command = "lamp" } = {}) {
614
+ const serial = await reach(place);
615
+ if (!serial) throw new Problem(`${place.name} isn't running: ${command} open --on ${place.word} starts it, with the page in it.`);
616
+ const mine = installedOn(layout, place);
617
+ if (!mine || !(await pidOf(mine.package, serial))) throw new Problem(`It isn't open on ${place.name}: ${command} open --on ${place.word} opens it there.`);
618
+ // Each is checked before any is made, so a move lamp can't make stops them all.
619
+ const inputs = moves.map(inputFor);
620
+ for (const [index, input] of inputs.entries()) {
621
+ if (!input) { await sleep(Math.min(Math.max(Number(moves[index].seconds) || 1, 0), 30) * 1000); continue; }
622
+ // A move goes to whatever is in front, and one before it may have left the app.
623
+ if (!(await inFront(mine.package, serial))) throw new Problem(`It isn't in front on ${place.name}${index ? `, after ${index} ${index === 1 ? "move" : "moves"}` : ""}: ${command} open --on ${place.word} brings it back.`);
624
+ const sent = await adb(["shell", "input", ...input], { serial, timeout: 60_000 });
625
+ if (!sent.ok) throw away(place, sent) ?? new Problem(`${place.name} didn't take that: ${tail(sent.out + sent.err, 4)}`);
626
+ }
627
+ // What the move did is drawn a moment after it.
628
+ await sleep(600);
629
+ return { did: `${moves.length === 1 ? "Done" : `${moves.length} moves made`} on ${place.name}.`, ...(await lookOnAndroid(layout, place, { command })) };
630
+ }
631
+
632
+ // MARK: The tools
633
+
634
+ const sha256 = (file) => new Promise((done, failed) => {
635
+ const hash = createHash("sha256");
636
+ createReadStream(file).on("data", (chunk) => hash.update(chunk)).on("end", () => done(hash.digest("hex"))).on("error", failed);
637
+ });
638
+
639
+ /** An archive that holds one folder with everything in it: that folder's contents become `folder`'s own. */
640
+ function flatten(folder) {
641
+ const names = readdirSync(folder).filter((name) => !name.startsWith("."));
642
+ if (names.length !== 1) return;
643
+ const inner = join(folder, names[0]);
644
+ let entries;
645
+ try { entries = readdirSync(inner); } catch { return; }
646
+ for (const name of entries) renameSync(join(inner, name), join(folder, name));
647
+ rmSync(inner, { recursive: true, force: true });
648
+ }
649
+
650
+ /**
651
+ * Downloads one release beside its place, checks it against what its maker published, unpacks it and moves it in
652
+ * whole. `part` hears how far along it is, from 0 to 1.
653
+ */
654
+ async function fetchInto(release, destination, archive, name, said, part = () => {}) {
655
+ const staging = join(dirname(destination), `.staging-${randomUUID()}`), download = join(staging, "download"), unpacked = join(staging, "unpacked");
656
+ mkdirSync(unpacked, { recursive: true });
657
+ try {
658
+ said(`Downloading ${name} (${Math.round(release.size / 1_000_000)} MB)…`);
659
+ part(0);
660
+ const answer = await fetch(release.url, { redirect: "follow" });
661
+ if (!answer.ok || !answer.body) throw new Problem(`${name} didn't download (${answer.status}).`);
662
+ await new Promise((done, failed) => {
663
+ const file = createWriteStream(download);
664
+ file.on("error", failed).on("finish", done);
665
+ (async () => {
666
+ let got = 0, told = 0;
667
+ try {
668
+ for await (const chunk of answer.body) {
669
+ got += chunk.length;
670
+ if (got - told > release.size / 100) { told = got; part(Math.min(got / release.size, 1) * 0.9); }
671
+ if (!file.write(chunk)) await new Promise((drained) => file.once("drain", drained));
672
+ }
673
+ file.end();
674
+ } catch (error) { file.destroy(); failed(new Problem(`${name} stopped downloading: ${error.message}`)); }
675
+ })();
676
+ });
677
+ said(`Unpacking ${name}…`);
678
+ part(0.9);
679
+ if ((await sha256(download)) !== release.sha256) throw new Problem(`The download of ${name} didn't match what its maker published, so lamp didn't use it.`);
680
+ const opened = archive === "tar" ? await run("/usr/bin/tar", ["-xzf", download, "-C", unpacked], { timeout: 600_000, env: process.env })
681
+ : await run("/usr/bin/unzip", ["-q", download, "-d", unpacked], { timeout: 600_000, env: process.env });
682
+ if (!opened.ok) throw new Problem(`lamp couldn't unpack ${name}.`);
683
+ rmSync(destination, { recursive: true, force: true });
684
+ renameSync(unpacked, destination);
685
+ } finally { rmSync(staging, { recursive: true, force: true }); }
686
+ }
687
+
688
+ /**
689
+ * Downloads and puts in place whichever of Android's tools are missing (`AndroidTools.install`): a Java runtime,
690
+ * Google's command-line tools and Gradle, each checked against the checksum its maker publishes, then through Google's
691
+ * own package manager the platform, and the emulator with its system image (`tv`: the TV's image too). Google's terms
692
+ * for its packages are accepted as they install, which is why only the person starts this.
693
+ */
694
+ export async function installTools({ emulator = true, tv = false, said = () => {}, progress = () => {} } = {}) {
695
+ const s = spec(), p = paths();
696
+ if (s.arch !== (process.arch === "x64" ? "x86_64" : process.arch)) throw new Problem("this copy of lamp has Android's tools for another kind of Mac: scripts/cli-files.sh writes them for this one");
697
+ mkdirSync(p.tools, { recursive: true });
698
+ mkdirSync(p.userHome, { recursive: true });
699
+ // How far along the whole is, weighed by size: the runtime and Gradle are small next to the platform's packages.
700
+ const between = (from, to) => (part) => progress(from + (to - from) * part);
701
+ let there = toolsThere();
702
+ if (!there.jdk) {
703
+ await fetchInto(s.jdk, p.jdk, "tar", "Java", said, between(0, 0.1));
704
+ // Temurin's archive holds one folder, jdk-17…/Contents/Home: that folder's contents are what's kept.
705
+ flatten(p.jdk);
706
+ if (!(await run(p.java, ["-version"])).ok) { rmSync(p.jdk, { recursive: true, force: true }); throw new Problem("Java didn't start on this Mac."); }
707
+ }
708
+ if (!there.commandLineTools) {
709
+ // Google's package manager wants itself at cmdline-tools/latest inside the SDK.
710
+ const latest = join(p.sdk, "cmdline-tools", "latest");
711
+ mkdirSync(dirname(latest), { recursive: true });
712
+ await fetchInto(s.commandLineTools, latest, "zip", "Google's tools", said, between(0.1, 0.18));
713
+ flatten(latest);
714
+ for (const tool of [p.sdkmanager, p.avdmanager]) chmodSync(tool, 0o755);
715
+ }
716
+ if (!there.gradle) {
717
+ await fetchInto(s.gradleRelease, p.gradleFolder, "zip", "Gradle", said, between(0.18, 0.26));
718
+ flatten(p.gradleFolder);
719
+ chmodSync(p.gradle, 0o755);
720
+ }
721
+ there = toolsThere();
722
+ const packages = [...(there.buildPackages ? [] : s.buildPackages), ...(emulator && !there.emulatorPackages ? s.emulatorPackages : []), ...(tv && !there.tvImage ? [s.tv.image] : [])];
723
+ if (packages.length) {
724
+ said(`Downloading ${emulator || tv ? "the Android platform and emulator" : "the Android platform"}, which takes a while…`);
725
+ // Google's terms have to be accepted for each package; sdkmanager asks on its input.
726
+ const script = '/usr/bin/yes | "$LAMP_SDKMANAGER" --licenses > /dev/null 2>&1; /usr/bin/yes | "$LAMP_SDKMANAGER" --install "$@" 2>&1';
727
+ const ran = await streamed("/bin/sh", ["-c", script, "sh", ...packages], { cwd: p.tools, env: { ...environment(), LAMP_SDKMANAGER: p.sdkmanager }, timeout: 2 * 3600_000 },
728
+ (line) => {
729
+ // sdkmanager draws its own bar; the number before its "%" says how far it is.
730
+ const percent = Number(/(\d{1,3})%/.exec(line)?.[1]);
731
+ if (percent >= 0 && percent <= 100) between(0.26, 0.96)(percent / 100);
732
+ });
733
+ there = toolsThere();
734
+ if (!there.buildPackages || (emulator && !there.emulatorPackages) || (tv && !there.tvImage)) throw new Problem(`Google's tools couldn't download the Android platform.\n${tail(ran.out, 6)}`);
735
+ }
736
+ progress(0.96);
737
+ return toolsThere();
738
+ }
739
+
740
+ /** Lets go of everything lamp made for Android: its emulators (closed first), and its key to the person's devices. The tools stay, every Lamp's. */
741
+ export async function forgetAndroid() {
742
+ if (knowsAndroid() && toolsThere().buildPackages) {
743
+ for (const kind of Object.keys(EMULATORS)) await closeEmulator(kind).catch(() => {});
744
+ }
745
+ const had = existsSync(androidHome());
746
+ rmSync(androidHome(), { recursive: true, force: true });
747
+ return had;
748
+ }
749
+
750
+ /** lamp's emulators, each with whether it's made and whether it's running now, for `lamp devices`. */
751
+ export async function emulators() {
752
+ const p = paths(), s = spec(), there = toolsThere(), all = [];
753
+ for (const kind of Object.keys(EMULATORS)) {
754
+ const downloaded = there.emulatorPackages && (kind !== "tv" || there.tvImage);
755
+ all.push({ kind, ...EMULATORS[kind], downloaded, made: existsSync(join(p.avdHome, `${s[kind].avd}.avd`, "config.ini")),
756
+ running: downloaded && there.buildPackages ? await emulatorRunning(kind) : false, hidden: !!readJSON(recordOf(kind))?.hidden });
757
+ }
758
+ return all;
759
+ }
760
+
761
+ export { advice as androidAdvice };
762
+
763
+ // MARK: What Lamp's app asks of adb
764
+
765
+ // adb for Lamp's app: every command it sends a device, through its own server and with its own key (`useLamps`), and
766
+ // what adb's words say read here, once, for lamp and for the app. What a device is to Lamp (a TV's record, which device
767
+ // Try On means) is the app's; what's shown on its screen (the emulator's window, the fingertip) too.
768
+
769
+ /** One adb command, as Lamp's app sends it: its words, and whether it ended well. */
770
+ export async function adbCommand(args, { serial, timeout = 60_000, input } = {}) {
771
+ const ran = await adb(args, { serial, timeout, input });
772
+ return { ok: ran.ok, out: ran.out, err: ran.err, timedOut: ran.timedOut };
773
+ }
774
+
775
+ /** Each line of `adb devices -l`: the selector adb takes with -s, and its state. */
776
+ export function rowsOf(listing) {
777
+ return String(listing).split("\n").slice(1).map((line) => line.trim().split(/\s+/)).filter((parts) => parts.length >= 2 && parts[0])
778
+ .map(([serial, state]) => ({ serial, state }));
779
+ }
780
+
781
+ const PROPERTY_KEYS = ["ro.product.manufacturer", "ro.product.model", "ro.product.device", "ro.build.version.release", "ro.build.version.sdk",
782
+ "ro.kernel.qemu", "ro.boot.qemu", "ro.serialno"];
783
+
784
+ /** What a device says of itself: maker, model, Android's version, whether it's a TV, and its screen. */
785
+ export function propertiesOf(said) {
786
+ const properties = {};
787
+ for (const line of String(said).split("\n").map((each) => each.trim())) {
788
+ const equals = line.indexOf("=");
789
+ if (equals >= 0) properties[line.slice(0, equals)] = line.slice(equals + 1);
790
+ else if (line.startsWith("Physical size:")) properties.size = line.slice("Physical size:".length).trim();
791
+ else if (line.startsWith("Physical density:")) properties.density = line.slice("Physical density:".length).trim();
792
+ }
793
+ return properties;
794
+ }
795
+
796
+ export async function deviceProperties(serial) {
797
+ // A TV says so by its leanback feature: Google TV, Android TV, Fire TV and the Android TV emulator alike.
798
+ const asked = PROPERTY_KEYS.map((key) => `echo ${key}=$(getprop ${key})`).join(";") + ";echo leanback=$(pm has-feature android.software.leanback);wm size;wm density";
799
+ return propertiesOf((await adb(["shell", asked], { serial, timeout: 20_000 })).out);
800
+ }
801
+
802
+ /** The devices adb can see, each with its state, what it says of itself when it's ready, and what adb's search sees on the network. */
803
+ export async function deviceListing() {
804
+ const listing = (await adb(["devices", "-l"], { timeout: 30_000 })).out, services = (await adb(["mdns", "services"], { timeout: 10_000 })).out;
805
+ const rows = rowsOf(listing);
806
+ for (const row of rows) if (row.state === "device") row.properties = await deviceProperties(row.serial);
807
+ return { listing, rows, services };
808
+ }
809
+
810
+ /** Connects adb to a device listening at "address:port": only once adb says it's connected. */
811
+ export async function connectTo(target) {
812
+ const said = (await adb(["connect", target], { timeout: 10_000 })).out.toLowerCase();
813
+ return said.includes("connected to") && !said.includes("failed") && !said.includes("cannot");
814
+ }
815
+
816
+ /** Pairs this computer's adb with a device by the code it shows, given on adb's input rather than in its arguments. */
817
+ export async function pairWith(host, port, code) {
818
+ const ran = await adb(["pair", `${host}:${port}`], { timeout: 30_000, input: `${code}\n` });
819
+ const said = (ran.out + ran.err).toLowerCase();
820
+ return said.includes("successfully paired") ? "paired" : said.includes("wrong password") ? "wrongCode" : "unanswered";
821
+ }
822
+
823
+ /**
824
+ * Puts the app on the device in place of the copy there. One signed with another key can't be updated: it's taken
825
+ * off first, and its data with it, since Android allows no other way.
826
+ */
827
+ export async function installApk(serial, apk, pkg) {
828
+ let ran = await adb(["install", "-r", "-t", apk], { serial, timeout: 300_000 });
829
+ if (!ran.ok && (ran.out + ran.err).includes("INSTALL_FAILED_UPDATE_INCOMPATIBLE")) {
830
+ await adb(["uninstall", pkg], { serial, timeout: 60_000 });
831
+ ran = await adb(["install", "-r", "-t", apk], { serial, timeout: 300_000 });
832
+ }
833
+ if (!ran.ok) throw new Problem(`Couldn't put the app on the device: ${(ran.out + ran.err).trim()}`);
834
+ }
835
+
836
+ /**
837
+ * Writes what the app is told about itself and Lamp into its own storage on the device: on adb's input, never in a
838
+ * command's arguments, and through `run-as`, which only a debuggable app allows, so it lands where only the app reads.
839
+ */
840
+ export async function tellApp(serial, pkg, values) {
841
+ const ran = await adb(["shell", `run-as ${pkg} sh -c 'mkdir -p files && cat > files/lamp-install.json'`], { serial, timeout: 30_000, input: JSON.stringify(values) });
842
+ return ran.ok ? null : (ran.out + ran.err).trim();
843
+ }
844
+
845
+ /** Asks the running app to save where the person is; true once it says it saved. */
846
+ export async function saveState(serial, pkg) {
847
+ const ran = await adb(["shell", "am", "broadcast", "-a", `${pkg}.LAMP_SAVE_STATE`, "-p", pkg], { serial, timeout: 10_000 });
848
+ return ran.ok && ran.out.includes('data="saved"');
849
+ }
850
+
851
+ /**
852
+ * What `pidof` says of an app: running (with its process), gone, or unreachable, when adb couldn't reach the device
853
+ * ("not found", "offline", "no devices") or said nothing in time, which isn't the app having quit.
854
+ */
855
+ export function processIn({ ok, out = "", err = "", timedOut = false }) {
856
+ const pid = Number(out.trim());
857
+ if (Number.isInteger(pid) && pid > 0 && /^\d+$/.test(out.trim())) return { state: "running", pid };
858
+ const said = (out + err).toLowerCase();
859
+ if (timedOut || /not found|offline|no devices|unauthorized|closed/.test(said)) return { state: "unreachable" };
860
+ return { state: "gone" };
861
+ }
862
+
863
+ export async function processOf(serial, pkg) {
864
+ return processIn(await adb(["shell", "pidof", pkg], { serial, timeout: 20_000 }));
865
+ }
866
+
867
+ /** Starts the app's main activity afresh; its process once it's running. */
868
+ export async function launchApp(serial, pkg) {
869
+ await adb(["shell", "am", "force-stop", pkg], { serial, timeout: 30_000 });
870
+ const ran = await adb(["shell", "am", "start", "-W", "-n", `${pkg}/.MainActivity`], { serial, timeout: 60_000 });
871
+ if (!ran.ok || ran.out.includes("Error")) throw new Problem(`The app didn't launch: ${(ran.out + ran.err).trim()}`);
872
+ for (let tries = 0; tries < 20; tries++) {
873
+ const now = await processOf(serial, pkg);
874
+ if (now.state === "running") return now.pid;
875
+ await sleep(250);
876
+ }
877
+ throw new Problem("The app started and quit at once.");
878
+ }
879
+
880
+ /** Android's own scale: 160 dpi is one pixel a point. */
881
+ export const scaleOf = (density) => (Number(density) > 0 ? Math.max(1, Number(density) / 160) : 1);
882
+
883
+ /** A picture of the screen as a PNG, made smaller by the device's density so a pixel in it is a point on the screen. */
884
+ export async function screenshotOf(serial, density) {
885
+ const shot = await adb(["exec-out", "screencap", "-p"], { serial, timeout: 60_000, binary: true });
886
+ if (!shot.ok || !sizeOf(shot.out)) return null;
887
+ const scale = scaleOf(density);
888
+ return scale > 1 ? resized(shot.out, sizeOf(shot.out).width / scale) ?? shot.out : shot.out;
889
+ }
890
+
891
+ /** The screen's size in pixels as the system draws it now: a size set with `wm size` over the panel's own. */
892
+ export async function displaySizeOf(serial) {
893
+ const sizes = [];
894
+ for (const line of (await adb(["shell", "wm", "size"], { serial, timeout: 20_000 })).out.split("\n")) {
895
+ const [label, value] = line.split(":");
896
+ const numbers = (value ?? "").trim().split("x").map(Number);
897
+ if (numbers.length === 2 && numbers.every((n) => n > 0)) sizes.push({ override: label.startsWith("Override"), width: numbers[0], height: numbers[1] });
898
+ }
899
+ const size = sizes.find((each) => each.override) ?? sizes[0];
900
+ return size ? { width: size.width, height: size.height } : null;
901
+ }
902
+
903
+ const UNESCAPE = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
904
+ const unescaped = (text) => text.replace(/&(#x?[0-9a-fA-F]+|[a-z]+);/g, (whole, name) =>
905
+ name[0] === "#" ? String.fromCodePoint(name[1] === "x" ? parseInt(name.slice(2), 16) : Number(name.slice(1))) : UNESCAPE[name] ?? whole);
906
+
907
+ /**
908
+ * What's on the screen, from the system's own dump of it (`uiautomator dump`): one item per thing the person can see
909
+ * or act on, in reading order, its frame in points (the dump's pixels over `scale`), each with an id of its kind's
910
+ * letter and its count ("b3"). A layout with no words of its own is just structure, and isn't one.
911
+ */
912
+ export function screenItems(xml, scale = 1) {
913
+ const items = [], counts = {};
914
+ for (const [, body] of String(xml).matchAll(/<node\b([^>]*?)\/?>/g)) {
915
+ const attributes = {};
916
+ for (const [, name, value] of body.matchAll(/([\w-]+)="([^"]*)"/g)) attributes[name] = unescaped(value);
917
+ const text = attributes.text ?? "", description = attributes["content-desc"] ?? "";
918
+ const className = (attributes.class ?? "").split(".").pop();
919
+ const clickable = attributes.clickable === "true", scrollable = attributes.scrollable === "true", checkable = attributes.checkable === "true";
920
+ const kind = { EditText: "field", Switch: "switch", ToggleButton: "switch", CheckBox: "checkbox", RadioButton: "checkbox", Button: "button" }[className]
921
+ ?? (className === "ImageView" || className === "ImageButton" ? (clickable ? "button" : "image")
922
+ : checkable ? "checkbox" : clickable ? "button" : scrollable ? "list" : "text");
923
+ if (!text && !description && !clickable && !checkable && className !== "EditText" && !scrollable) continue;
924
+ const numbers = (attributes.bounds ?? "").split(/[[\],]/).filter(Boolean).map(Number);
925
+ if (numbers.length !== 4 || numbers.some((n) => !Number.isFinite(n))) continue;
926
+ const frame = { x: numbers[0] / scale, y: numbers[1] / scale, width: (numbers[2] - numbers[0]) / scale, height: (numbers[3] - numbers[1]) / scale };
927
+ if (frame.width <= 0 || frame.height <= 0) continue;
928
+ const name = description || (kind === "field" ? attributes.hint ?? "" : text);
929
+ const value = kind === "field" || description ? text : "";
930
+ counts[kind[0]] = (counts[kind[0]] ?? 0) + 1;
931
+ items.push({ id: `${kind[0]}${counts[kind[0]]}`, kind, name, value: value === name ? "" : value, frame,
932
+ checked: checkable ? attributes.checked === "true" : null, enabled: attributes.enabled !== "false", clickable, scrollable, focused: attributes.focused === "true" });
933
+ }
934
+ return items;
935
+ }
936
+
937
+ /** The dump between its `<?xml` and its `</hierarchy>`, or null when it has none (a screen asleep, or still being laid out). */
938
+ export function dumpIn(said) {
939
+ const start = String(said).indexOf("<?xml"), end = String(said).indexOf("</hierarchy>");
940
+ return start >= 0 && end > start ? String(said).slice(start, end + "</hierarchy>".length) : null;
941
+ }