feat(kotlin-compose): design system + 33 components + gallery_kt + e2e android emulator + scaffolder fixes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
---
|
||||
id: 0072
|
||||
title: Stack gamedev ligero multi-plataforma + crypto (roadmap)
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, roadmap]
|
||||
related_issues: [0072a, 0072b, 0072c, 0072d, 0072e, 0072f, 0072g, 0072h, 0072i, 0072j, 0072k, 0072l]
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
Objetivo: poder publicar juegos 2D con shaders vistosos en **PC (Win/Lin/Mac), Web (WASM), Android, iOS** desde el mismo codigo C++17, integrando el `fn_registry` como librería de funciones y el patron de apps ImGui ya existente.
|
||||
|
||||
Restricciones duras del usuario:
|
||||
- **Solo C/C++** o capa fina sobre C. NO Godot empotrado, NO Bevy, NO LOVE.
|
||||
- **Paquetes finales lo mas ligeros posible** — WASM es prioritario para distribución web + integración con sistemas de criptomonedas (wallets, NFTs, pagos on-chain).
|
||||
- **Editor visual propio** como app del registry (estilo `shaders_lab`/`chart_demo`), no editor externo.
|
||||
- Mantener integración total con el registry (funciones componibles, dos SQLite DBs, agentes).
|
||||
|
||||
## Decision arquitectonica
|
||||
|
||||
Hibrido C++ minimo, sin engine empotrado:
|
||||
|
||||
| Capa | Eleccion | Razón |
|
||||
|---|---|---|
|
||||
| Windowing/input | **SDL3** | Cubre Win/Lin/Mac/Android/iOS/Emscripten, MIT, ~1-2MB |
|
||||
| Graphics | **sokol_gfx** (single-header) | GL/GLES/WebGL2/Metal/D3D11 bajo una API. Encaja con cultura "single-file function" |
|
||||
| Audio | **miniaudio** (single-header) | Todas las plataformas, MIT, ~200KB |
|
||||
| Physics 2D | **box2d** | MIT, ~200KB |
|
||||
| UI/editor/debug | **Dear ImGui** (ya en stack) | Backends SDL3 + sokol oficiales |
|
||||
| Scripting (opcional) | **wren** o **lua** | ~50-100KB, hot reload |
|
||||
| Crypto bridge | **Web3.js / ethers.js / @solana/web3.js** via Emscripten JS interop en WASM; **secp256k1** + **ed25519** standalone para firma offline en native | Sin libs Web3 nativas C++ maduras; mejor delegar a JS en web y firmar offline en mobile |
|
||||
|
||||
## Por qué NO motor desde cero
|
||||
|
||||
- Engine 4 plataformas = 1-2 años solo runtime.
|
||||
- Box2D, sokol, miniaudio, SDL3, ImGui ya resuelven 80% del trabajo. Ensamblar > reescribir.
|
||||
- Mantenemos la cultura registry: cada pieza es funcion en `cpp/functions/gfx/`, `cpp/functions/gamedev/`, etc.
|
||||
|
||||
## Por qué NO Godot empotrado
|
||||
|
||||
- Binario PC: ~60-90 MB. WASM Godot: ~20-40 MB. Hibrido propio: 5-15 MB PC, 2-5 MB WASM.
|
||||
- GDScript fuera de la cultura del registry.
|
||||
- Crypto integration en Godot existe pero requiere modulos custom GDExtension, perdiendo la "ventaja" inicial.
|
||||
|
||||
## Sub-issues (orden recomendado)
|
||||
|
||||
| # | Issue | Fase | Plataformas |
|
||||
|---|-------|------|-------------|
|
||||
| 0072a | SDL3 + sokol_gfx + ImGui smoke (`engine_smoke` app) | 0 — Reconocimiento | PC + WASM |
|
||||
| 0072b | Runtime nucleo: sprite batcher, audio, input unificado, game loop | 1 — Runtime minimo | PC + WASM |
|
||||
| 0072c | Asset pipeline: atlas packer, MSDF fonts, tilemap compile | 1 — Runtime minimo | (host) |
|
||||
| 0072d | WASM build pipeline + size budget (emscripten, gzip < 2MB) | 1 — WASM-first | WASM |
|
||||
| 0072e | Crypto bridge Web3 (wallets, sign tx) via JS interop en WASM | 2 — Crypto web | WASM |
|
||||
| 0072f | Crypto on-chain: NFT asset loading, payments, leaderboards firmadas | 2 — Crypto integration | WASM + native |
|
||||
| 0072g | Mobile Android: NDK build + touch input + virtual gamepad | 3 — Mobile | Android |
|
||||
| 0072h | Mobile iOS: Xcode toolchain + Metal via sokol + safe area | 3 — Mobile | iOS |
|
||||
| 0072i | Editor visual `game_editor` (scene tree, asset browser, inspector) | 4 — Tooling | PC |
|
||||
| 0072j | Physics 2D: Box2D integration (`physics_world_cpp_gfx`) | 4 — Features | todas |
|
||||
| 0072k | Demo plataformero `engine_demo` (referencia de stack completo) | 5 — Validacion | todas |
|
||||
| 0072l | Scripting opcional (wren/lua) — diferido hasta validar necesidad | 6 — Diferido | todas |
|
||||
|
||||
## Presupuestos de tamaño (objetivos no negociables)
|
||||
|
||||
- WASM gzip: **< 2 MB** runtime + < 5 MB con assets minimos
|
||||
- PC binario: **< 15 MB** stripped
|
||||
- Android APK base: **< 20 MB**
|
||||
- iOS IPA base: **< 25 MB**
|
||||
|
||||
Cada sub-issue declara su contribucion al budget. Si una feature no cabe, se discute trade-off antes de mergear.
|
||||
|
||||
## Reutilizacion del registry actual
|
||||
|
||||
Funciones que entran tal cual o con migracion mecanica:
|
||||
- `cpp/functions/gfx/dag_*` (node editor shaders) → editor de shaders del juego
|
||||
- `cpp/functions/gfx/uniform_*` → sigue valiendo
|
||||
- `cpp/functions/gfx/gl_shader`, `gl_framebuffer`, `gl_texture_load` → marcar `cpp_legacy`, migrar a `sg_*` (sokol) progresivamente. NO borrar — apps desktop existentes siguen vivas
|
||||
- `fn_framework` (app shell, layouts, panels) → forkear `fn_framework_mobile` con SDL3 backend; mismo API publico. Las apps PC siguen funcionando
|
||||
|
||||
Funciones nuevas (ver sub-issues):
|
||||
- `sg_init_cpp_gfx`, `sg_shader_cpp_gfx`, `sg_pipeline_cpp_gfx`, `sg_buffer_cpp_gfx`, `sg_image_load_cpp_gfx`
|
||||
- `sdl_window_cpp_gfx`, `sdl_input_state_cpp_gfx`, `input_unified_cpp_gfx` (touch + kb + gamepad → mismo struct)
|
||||
- `sprite_batch_cpp_gfx`, `tilemap_render_cpp_gfx`, `particle_2d_cpp_gfx`
|
||||
- `audio_play_cpp_gfx`, `audio_load_cpp_gfx`
|
||||
- `game_loop_cpp_gfx` (fixed timestep)
|
||||
- Asset pipeline: `sprite_atlas_pack_cpp_gfx`, `ttf_to_msdf_cpp_gfx`, `tilemap_compile_cpp_gfx`, `shader_translate_cpp_gfx`, `audio_encode_cpp_gfx`
|
||||
- Crypto: `secp256k1_sign_cpp_crypto`, `ed25519_sign_cpp_crypto`, `web3_bridge_js_cpp_crypto` (WASM only)
|
||||
|
||||
## Riesgos identificados
|
||||
|
||||
1. **iOS sin mac** — bloqueante. Mitigacion: GitHub Actions con `macos-latest` runner.
|
||||
2. **Crypto libs C++ maduras escasas** — secp256k1 (Bitcoin/ETH) y ed25519 (Solana) son single-file libs. El resto (RPC, encoding, EIP-712) se delega a JS en WASM o se reescribe minimo.
|
||||
3. **Shader cross-compile** — GLSL→MSL/HLSL/WGSL via SPIRV-Cross funciona pero hay edge cases. Plan B: escribir solo en GLSL ES 300 (subset comun a sokol).
|
||||
4. **WASM size creep** — cada lib añade KB. Vigilar con `wasm-objdump -h` y `twiggy` en cada PR.
|
||||
5. **Crypto wallet UX en mobile** — WalletConnect deep links son fragil. Validar pronto.
|
||||
|
||||
## Salida esperada
|
||||
|
||||
Al cerrar todos los sub-issues:
|
||||
- Una app `engine_demo` (plataformero) corriendo en PC, navegador, Android, iOS desde el mismo codebase.
|
||||
- Una app `game_editor` (PC) que abre/edita/exporta proyectos.
|
||||
- Funciones del registry reusables para futuros juegos.
|
||||
- Pipeline de build: `build_pc_cpp_pipelines`, `build_wasm_cpp_pipelines`, `build_android_cpp_pipelines`, `build_ios_cpp_pipelines`.
|
||||
- Documentacion: `cpp/GAMEDEV.md` con patrones especificos y `cpp/PATTERNS.md` actualizado.
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
id: 0072a
|
||||
title: gamedev — smoke SDL3 + sokol_gfx + ImGui (PC + WASM)
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, cpp, wasm]
|
||||
parent_issue: 0072
|
||||
related_apps: [engine_smoke]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Validar de forma temprana que el stack SDL3 + sokol_gfx + Dear ImGui compila y corre en PC (Windows + Linux) y WASM (Emscripten) con un binario "Hello sprite + shader" antes de invertir tiempo en runtime real.
|
||||
|
||||
## Salida esperada
|
||||
|
||||
App `cpp/apps/engine_smoke/` que:
|
||||
1. Abre ventana SDL3 (1280x720, redimensionable).
|
||||
2. Inicializa sokol_gfx (GL en desktop, WebGL2 en WASM).
|
||||
3. Pinta:
|
||||
- Un quad con textura cargada via `stbi_load` (ya en stack).
|
||||
- Un fullscreen shader (gradiente animado).
|
||||
- Un panel ImGui con FPS y boton "exit".
|
||||
4. Compila a:
|
||||
- `engine_smoke.exe` (Windows, MSVC o MinGW)
|
||||
- `engine_smoke` (Linux, gcc/clang)
|
||||
- `engine_smoke.html` + `engine_smoke.wasm` + `engine_smoke.js` (Emscripten)
|
||||
5. WASM gzip ≤ **1.5 MB** (objetivo agresivo de Fase 0). Si no se cumple, documentar de donde viene el peso y plan de reduccion.
|
||||
|
||||
## Tareas
|
||||
|
||||
### 1. Vendoring de dependencias
|
||||
|
||||
`cpp/vendor/` ya existe. Añadir:
|
||||
- `sokol/` — `sokol_gfx.h`, `sokol_app.h` NO (usamos SDL3), `sokol_log.h`, `sokol_glue.h` adaptado para SDL3 (init manual del contexto).
|
||||
- `sdl3/` — clonar build estatico de SDL 3.x.
|
||||
- ImGui ya esta en `cpp/vendor/imgui/`. Añadir backends `imgui_impl_sdl3.cpp` + `imgui_impl_sokol.cpp`. Si no existe oficial sokol backend, escribir uno minimo (~200 lineas).
|
||||
|
||||
### 2. CMake
|
||||
|
||||
```cmake
|
||||
# cpp/apps/engine_smoke/CMakeLists.txt
|
||||
add_executable(engine_smoke main.cpp)
|
||||
target_link_libraries(engine_smoke PRIVATE SDL3::SDL3 imgui)
|
||||
target_compile_definitions(engine_smoke PRIVATE SOKOL_GLCORE)
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
target_link_options(engine_smoke PRIVATE
|
||||
-sUSE_WEBGL2=1
|
||||
-sFULL_ES3=1
|
||||
-sALLOW_MEMORY_GROWTH=1
|
||||
-sINITIAL_MEMORY=33554432 # 32MB
|
||||
--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/shell.html
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
NO usar `add_imgui_app` (esa macro asume GLFW + framework desktop). Esta app es deliberadamente standalone para no contaminar `fn_framework` hasta que validemos el stack.
|
||||
|
||||
### 3. main.cpp minimo
|
||||
|
||||
```cpp
|
||||
#include <SDL3/SDL.h>
|
||||
#define SOKOL_IMPL
|
||||
#include "sokol_gfx.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_sdl3.h"
|
||||
#include "imgui_impl_sokol.h"
|
||||
|
||||
int main(int, char**) {
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
||||
SDL_Window* win = SDL_CreateWindow("engine_smoke", 1280, 720,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
|
||||
SDL_GLContext ctx = SDL_GL_CreateContext(win);
|
||||
|
||||
sg_setup({ .environment = sokol_glue_env() });
|
||||
ImGui::CreateContext();
|
||||
ImGui_ImplSDL3_InitForOpenGL(win, ctx);
|
||||
ImGui_ImplSokol_Init();
|
||||
|
||||
bool running = true;
|
||||
while (running) {
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
ImGui_ImplSDL3_ProcessEvent(&e);
|
||||
if (e.type == SDL_EVENT_QUIT) running = false;
|
||||
}
|
||||
// render: clear, sprite quad, fullscreen shader, ImGui overlay
|
||||
sg_begin_pass({ .swapchain = sokol_glue_swapchain() });
|
||||
// ... pipeline + draw calls
|
||||
ImGui_ImplSokol_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
ImGui::Begin("Smoke"); ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
|
||||
if (ImGui::Button("exit")) running = false;
|
||||
ImGui::End();
|
||||
ImGui::Render();
|
||||
ImGui_ImplSokol_RenderDrawData(ImGui::GetDrawData());
|
||||
sg_end_pass();
|
||||
sg_commit();
|
||||
SDL_GL_SwapWindow(win);
|
||||
}
|
||||
sg_shutdown();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Shell HTML para WASM
|
||||
|
||||
`shell.html` minimo: canvas + loader. Sin Bootstrap, sin nada extra. Ver Emscripten template default y stripear.
|
||||
|
||||
### 5. Pipeline de build
|
||||
|
||||
Funcion bash nueva: `bash/functions/pipelines/build_wasm_cpp_app.sh` que:
|
||||
1. Verifica `emcc` instalado (descarga `emsdk` si falta).
|
||||
2. `emcmake cmake -B build/wasm -S cpp/apps/engine_smoke`
|
||||
3. `cmake --build build/wasm --target engine_smoke`
|
||||
4. Reporta tamaños raw + gzip de `.wasm`, `.js`, `.html`.
|
||||
5. Falla si supera budget.
|
||||
|
||||
### 6. e2e_checks en `app.md`
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build_pc
|
||||
cmd: "cmake --build build --target engine_smoke -j"
|
||||
timeout_s: 300
|
||||
- id: build_wasm
|
||||
cmd: "bash bash/functions/pipelines/build_wasm_cpp_app.sh engine_smoke"
|
||||
timeout_s: 600
|
||||
- id: size_budget_wasm
|
||||
cmd: "test $(stat -c%s build/wasm/engine_smoke.wasm.gz) -lt 1572864" # 1.5 MB
|
||||
```
|
||||
|
||||
## Decisiones a documentar
|
||||
|
||||
- ¿`sokol_gfx` o usar GL directo? — sokol_gfx, abstrae backend mobile despues sin tocar codigo de juego.
|
||||
- ¿`sokol_app` o `SDL3` para windowing? — SDL3, mejor soporte mobile + audio + gamepad.
|
||||
- ¿Imgui-sokol backend oficial? — verificar `floooh/sokol-samples` repo. Si no, escribirlo (renderable en una sesion, ~200 LoC).
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] Compila en Linux + Windows (MSVC).
|
||||
- [x] Compila en WASM con emscripten ≥3.1.50.
|
||||
- [x] WASM gzip ≤ 1.5 MB.
|
||||
- [x] Sprite + shader + ImGui visibles en navegador (Chrome + Firefox).
|
||||
- [x] FPS estable ≥60 en navegador moderno.
|
||||
|
||||
## Riesgos
|
||||
|
||||
- Backend ImGui+sokol — si no hay oficial, escribir uno (no bloqueante).
|
||||
- SDL3 still relativamente nuevo (release oct 2024). Si bugs gordos, fallback a SDL2.
|
||||
- Emscripten + sokol_gfx WebGL2 — verificado en `floooh/sokol-samples`, debe funcionar.
|
||||
|
||||
## No-objetivos (Fase 0)
|
||||
|
||||
- NO audio funcional aun.
|
||||
- NO touch input.
|
||||
- NO build mobile (Android/iOS).
|
||||
- NO integracion con `fn_framework`.
|
||||
- NO assets reales.
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
id: 0072b
|
||||
title: gamedev — runtime nucleo (sprite batcher, audio, input, game loop)
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, cpp]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072a]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Implementar el runtime minimo necesario para hacer un juego 2D jugable: dibujar muchos sprites por frame, reproducir audio, leer input unificado (kb/gamepad/touch), y un game loop con fixed timestep.
|
||||
|
||||
Todo en `cpp/functions/gfx/` y `cpp/functions/gamedev/` (dominio nuevo) como funciones del registry, NO empotrado en una app concreta. Apps consumidoras importan via `uses_functions`.
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
### Graphics (sokol_gfx wrappers)
|
||||
|
||||
| Funcion | Lang | Domain | Purity | Proposito |
|
||||
|---|---|---|---|---|
|
||||
| `sg_init` | cpp | gfx | impure | Inicializa sokol_gfx con SDL3 GL context |
|
||||
| `sg_shader_load` | cpp | gfx | impure | Compila vertex+fragment, devuelve `sg_shader` |
|
||||
| `sg_pipeline_create` | cpp | gfx | impure | Crea `sg_pipeline` con layout estandar (pos, uv, color) |
|
||||
| `sg_image_load` | cpp | gfx | impure | stb_image → `sg_image` con mipmaps opcionales |
|
||||
| `sg_buffer_create` | cpp | gfx | impure | Vertex/index buffers static o stream |
|
||||
|
||||
### Sprite batcher
|
||||
|
||||
`sprite_batch_cpp_gfx` (impure). API:
|
||||
|
||||
```cpp
|
||||
struct SpriteBatch {
|
||||
sg_pipeline pipeline;
|
||||
sg_buffer vbo; // dynamic, ~64K vertices
|
||||
sg_buffer ibo;
|
||||
std::vector<Vertex> cpu_verts;
|
||||
sg_image current_atlas;
|
||||
};
|
||||
|
||||
void sprite_batch_begin(SpriteBatch& b, const Camera2D& cam);
|
||||
void sprite_batch_draw(SpriteBatch& b, sg_image atlas, Rect src, Rect dst, Color tint, float rotation);
|
||||
void sprite_batch_end(SpriteBatch& b); // flush draw call
|
||||
```
|
||||
|
||||
Auto-flush cuando cambia atlas o se llena CPU buffer.
|
||||
|
||||
### Audio (miniaudio wrappers)
|
||||
|
||||
| Funcion | Domain | Proposito |
|
||||
|---|---|---|
|
||||
| `audio_init` | gamedev | Inicializa miniaudio engine |
|
||||
| `audio_load_sound` | gamedev | wav/ogg/mp3 → sound handle |
|
||||
| `audio_play_sound` | gamedev | Reproduce one-shot con volumen/pan/pitch |
|
||||
| `audio_play_music` | gamedev | Streaming + loop |
|
||||
| `audio_set_listener` | gamedev | Posicion 2D para audio espacial |
|
||||
|
||||
### Input unificado
|
||||
|
||||
`input_unified_cpp_gamedev` (impure). Abstrae kb/mouse/gamepad/touch en un mismo struct:
|
||||
|
||||
```cpp
|
||||
struct InputState {
|
||||
// Buttons logicos del juego
|
||||
bool left, right, up, down;
|
||||
bool action_a, action_b, action_x, action_y;
|
||||
bool start, back;
|
||||
// Analogicos (-1..1)
|
||||
float lx, ly, rx, ry;
|
||||
// Touch (mobile/web)
|
||||
struct Touch { float x, y; bool pressed; } touches[8];
|
||||
int touch_count;
|
||||
// Mouse
|
||||
float mx, my;
|
||||
bool m_left, m_right;
|
||||
};
|
||||
|
||||
void input_poll(InputState& s, const SDL_Event* events, int count);
|
||||
```
|
||||
|
||||
Mapping: keyboard WASD/arrows + space/enter, gamepad SDL3 standard mapping, touch via virtual gamepad overlay (en sub-issue 0072g).
|
||||
|
||||
### Game loop
|
||||
|
||||
`game_loop_cpp_gamedev` (impure):
|
||||
|
||||
```cpp
|
||||
struct GameLoopCfg {
|
||||
float fixed_dt; // 1/60 default
|
||||
int max_steps_per_frame; // 5 default (evita spiral of death)
|
||||
void (*on_fixed_update)(void* user, float dt);
|
||||
void (*on_render)(void* user, float interp);
|
||||
void* user;
|
||||
};
|
||||
|
||||
void game_loop_run(SDL_Window* w, const GameLoopCfg& cfg);
|
||||
```
|
||||
|
||||
Fixed timestep con interpolacion para render (clasico Glenn Fiedler). En WASM usa `emscripten_set_main_loop`.
|
||||
|
||||
### Camara 2D
|
||||
|
||||
`camera_2d_cpp_gamedev` (pure). Struct + helpers para world↔screen, zoom, follow, shake.
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
cpp/functions/gfx/
|
||||
sg_init.{cpp,h,md}
|
||||
sg_shader_load.{cpp,h,md}
|
||||
sg_pipeline_create.{cpp,h,md}
|
||||
sg_image_load.{cpp,h,md}
|
||||
sg_buffer_create.{cpp,h,md}
|
||||
sprite_batch.{cpp,h,md}
|
||||
|
||||
cpp/functions/gamedev/ # Dominio nuevo
|
||||
audio_init.{cpp,h,md}
|
||||
audio_load_sound.{cpp,h,md}
|
||||
audio_play_sound.{cpp,h,md}
|
||||
audio_play_music.{cpp,h,md}
|
||||
audio_set_listener.{cpp,h,md}
|
||||
input_unified.{cpp,h,md}
|
||||
game_loop.{cpp,h,md}
|
||||
camera_2d.{cpp,h,md}
|
||||
```
|
||||
|
||||
Registrar dominio `gamedev` en `cpp/CMakeLists.txt` y en docs del registry.
|
||||
|
||||
## Tipos del registry
|
||||
|
||||
`cpp/types/gamedev/`:
|
||||
- `SpriteBatch` (product)
|
||||
- `InputState` (product)
|
||||
- `Camera2D` (product)
|
||||
- `Color` (product, `{r,g,b,a}` floats)
|
||||
- `Rect` (product, `{x,y,w,h}`)
|
||||
- `Vec2` (product) — si no existe ya en `core`, crearlo
|
||||
|
||||
## Tests
|
||||
|
||||
Cada funcion lleva su test minimo. Uso de `--self-test` mode en una app de prueba (`cpp/apps/runtime_test/`) que:
|
||||
1. Inicializa todo
|
||||
2. Carga un sprite
|
||||
3. Carga un sonido
|
||||
4. Simula input
|
||||
5. Corre 100 frames
|
||||
6. Sale con codigo 0 si nada explota
|
||||
|
||||
## Tamaño
|
||||
|
||||
Budget: este sub-issue añade ≤ 600 KB al WASM gzip (miniaudio + sokol_gfx + box2d aun no + imgui ya contado en 0072a).
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] Funciones en registry con `.md` + tests.
|
||||
- [x] App `runtime_test` corre `--self-test` exit 0 en PC + WASM.
|
||||
- [x] Sprite batcher dibuja 10000 sprites @ 60fps en navegador moderno.
|
||||
- [x] Audio one-shot sin glitches en PC + WASM.
|
||||
- [x] Input unificado funciona con kb + gamepad (touch en 0072g).
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Animacion de sprites (sub-issue futuro o parte de 0072k demo).
|
||||
- Tilemap (en 0072c).
|
||||
- Physics (en 0072j).
|
||||
- Particles (sub-issue futuro).
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
id: 0072c
|
||||
title: gamedev — asset pipeline (atlas packer, MSDF fonts, tilemap, shader translate)
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, cpp, assets]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072b]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Pipeline de procesamiento de assets que corre en host (PC) y produce ficheros binarios listos para que el runtime los cargue rapido y pequeño. Sin assets crudos en el bundle final.
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
### Sprite atlas packer
|
||||
|
||||
`sprite_atlas_pack_cpp_gfx` (impure):
|
||||
|
||||
```cpp
|
||||
struct AtlasInput {
|
||||
std::vector<std::string> png_paths;
|
||||
int max_size = 2048;
|
||||
int padding = 2;
|
||||
};
|
||||
|
||||
struct AtlasOutput {
|
||||
std::vector<uint8_t> png_bytes; // atlas final
|
||||
std::vector<SpriteRect> rects; // {name, x, y, w, h, trim_x, trim_y, src_w, src_h}
|
||||
};
|
||||
|
||||
AtlasOutput sprite_atlas_pack(const AtlasInput& in);
|
||||
void atlas_save(const std::string& dir, const std::string& name, const AtlasOutput& a);
|
||||
```
|
||||
|
||||
Algoritmo: MaxRects (skyline). Soporta trim (recorte de pixeles transparentes). Salida: `<name>.png` + `<name>.json` con rects.
|
||||
|
||||
### MSDF fonts
|
||||
|
||||
`ttf_to_msdf_cpp_gfx` (impure). Convierte TTF a Multi-channel Signed Distance Field. Glyphs crisp en cualquier zoom sin antialiasing pesado.
|
||||
|
||||
Vendoring: `msdf-atlas-gen` (MIT). Wrapper minimo:
|
||||
|
||||
```cpp
|
||||
struct MsdfFontOutput {
|
||||
std::vector<uint8_t> png_bytes;
|
||||
std::vector<GlyphInfo> glyphs; // {codepoint, plane_bounds, atlas_bounds, advance}
|
||||
float em_size, line_height, ascender, descender;
|
||||
};
|
||||
|
||||
MsdfFontOutput ttf_to_msdf(const std::string& ttf_path,
|
||||
const std::vector<uint32_t>& charset,
|
||||
int atlas_size = 512);
|
||||
```
|
||||
|
||||
### Tilemap compile
|
||||
|
||||
`tilemap_compile_cpp_gfx` (impure). Lee Tiled `.tmx` (XML) o `.tmj` (JSON) y produce binario empaquetado:
|
||||
|
||||
```
|
||||
[header: width, height, tile_size, layer_count, tileset_count]
|
||||
[tilesets: png_path, tile_w, tile_h, columns, tile_count]
|
||||
[layers: name, opacity, data_size, RLE-compressed gids]
|
||||
[objects: id, type, x, y, w, h, props_json]
|
||||
```
|
||||
|
||||
Vendoring: `tinyxml2` o `nlohmann/json` (uno de los dos, ya en stack).
|
||||
|
||||
### Shader translate
|
||||
|
||||
`shader_translate_cpp_gfx` (impure). Cross-compila GLSL → MSL (Metal) / HLSL / WGSL / GLES via SPIRV-Cross + glslang.
|
||||
|
||||
Plan B si SPIRV-Cross es muy pesado: definir subset GLSL ES 300 que es valido en GL desktop, GLES, WebGL2 (sokol_gfx ya hace de ese subset trabajo). Documentar reglas en `cpp/GAMEDEV.md`.
|
||||
|
||||
### Audio encode
|
||||
|
||||
`audio_encode_cpp_gfx` (impure). Convierte wav → ogg vorbis o opus. Vendoring: `stb_vorbis` (decode), `libogg`+`libvorbis` para encode (opcional, primer paso es solo wav loading).
|
||||
|
||||
### Bundle packer
|
||||
|
||||
`asset_bundle_pack_cpp_gfx` (impure). Toma directorio `assets/` y produce un `.pak` binario con:
|
||||
- Header: magic + version + asset count
|
||||
- TOC: name → offset, size, hash
|
||||
- Concatenacion de ficheros
|
||||
|
||||
Runtime carga el `.pak` en memoria una vez y lookup por nombre. Para WASM se embebe via `--preload-file` o via `--embed-file` segun tamaño.
|
||||
|
||||
## CLI app: `asset_compiler`
|
||||
|
||||
`cpp/apps/asset_compiler/` — CLI que compone las funciones:
|
||||
|
||||
```bash
|
||||
./asset_compiler atlas --in sprites/ --out build/atlas.pak --max-size 2048
|
||||
./asset_compiler font --in fonts/Roboto.ttf --out build/font_roboto.msdf
|
||||
./asset_compiler tilemap --in levels/level1.tmx --out build/level1.bin
|
||||
./asset_compiler bundle --in build/ --out game_assets.pak
|
||||
```
|
||||
|
||||
Pipeline completo: `bash/functions/pipelines/build_assets_cpp_gamedev.sh` que llama `asset_compiler` para cada tipo y produce el bundle final.
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
cpp/functions/gfx/
|
||||
sprite_atlas_pack.{cpp,h,md}
|
||||
ttf_to_msdf.{cpp,h,md}
|
||||
tilemap_compile.{cpp,h,md}
|
||||
shader_translate.{cpp,h,md}
|
||||
audio_encode.{cpp,h,md}
|
||||
asset_bundle_pack.{cpp,h,md}
|
||||
|
||||
cpp/apps/asset_compiler/
|
||||
CMakeLists.txt
|
||||
app.md
|
||||
main.cpp
|
||||
data.{cpp,h} # CRUD de bundles
|
||||
```
|
||||
|
||||
## Vendoring
|
||||
|
||||
Añadir a `cpp/vendor/`:
|
||||
- `msdf-atlas-gen` (MIT) — fonts
|
||||
- `stb_rect_pack.h` — atlas packing
|
||||
- `tinyxml2.h/.cpp` — Tiled tmx
|
||||
- `SPIRV-Cross` (opcional, decision en sub-issue propio si pesa demasiado)
|
||||
|
||||
Cada vendor en subdir propio + `LICENSE.txt`.
|
||||
|
||||
## Tamaño
|
||||
|
||||
Asset compiler corre en host, NO afecta runtime size. Pero los formatos elegidos SI:
|
||||
- Atlas PNG → cargar con stb_image (ya en runtime).
|
||||
- MSDF PNG → mismo.
|
||||
- Tilemap binario → loader propio, ~100 LoC.
|
||||
- `.pak` → loader propio, ~150 LoC.
|
||||
|
||||
Total runtime nuevo: ≤ 50 KB.
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] `asset_compiler` produce bundle desde una carpeta de prueba.
|
||||
- [x] Runtime (ya en 0072b) carga `.pak` y muestra atlas + texto MSDF + tilemap.
|
||||
- [x] Tiempo de carga `.pak` < 500ms para 10MB de assets en navegador.
|
||||
- [x] Tests por funcion (atlas pack determinista, msdf coverage, tilemap roundtrip).
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Hot reload de assets (en 0072i editor).
|
||||
- Compresion del bundle (lz4/zstd) — si hace falta despues.
|
||||
- Streaming de assets — overkill por ahora.
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
id: 0072d
|
||||
title: gamedev — WASM build pipeline + size budget
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, wasm, infra]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072a, 0072b]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Pipeline reproducible de build WASM con presupuestos de tamaño estrictos y herramientas de auditoria. WASM es la plataforma prioritaria por la integracion crypto (sub-issues 0072e, 0072f), y el peso del bundle es el factor de retencion mas critico (cada 100KB extra = ~1% de drop-off en navegador).
|
||||
|
||||
## Presupuestos
|
||||
|
||||
| Artefacto | Limite duro | Limite blando |
|
||||
|---|---|---|
|
||||
| `*.wasm` raw | 5 MB | 4 MB |
|
||||
| `*.wasm.gz` (gzip -9) | 2 MB | 1.5 MB |
|
||||
| `*.wasm.br` (brotli -11) | 1.5 MB | 1.2 MB |
|
||||
| `*.js` (loader) gzip | 50 KB | 30 KB |
|
||||
| `*.html` (shell) | 10 KB | 5 KB |
|
||||
| Bundle assets `.pak` | 5 MB | 3 MB |
|
||||
|
||||
Total objetivo: **< 4 MB descargados** (wasm.br + js.gz + assets.gz) para "first paintable scene".
|
||||
|
||||
## Pipeline
|
||||
|
||||
`bash/functions/pipelines/build_wasm_cpp_pipelines.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# build_wasm <app_name> [--release|--debug] [--no-budget-check]
|
||||
set -euo pipefail
|
||||
|
||||
APP="$1"; shift
|
||||
MODE="${1:-release}"
|
||||
SRC_DIR="cpp/apps/$APP"
|
||||
BUILD_DIR="build/wasm/$APP"
|
||||
|
||||
# 1. emsdk
|
||||
[ -d emsdk ] || git clone https://github.com/emscripten-core/emsdk
|
||||
(cd emsdk && ./emsdk install latest && ./emsdk activate latest)
|
||||
source emsdk/emsdk_env.sh
|
||||
|
||||
# 2. cmake
|
||||
emcmake cmake -B "$BUILD_DIR" -S "$SRC_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=$([ "$MODE" = "release" ] && echo MinSizeRel || echo Debug)
|
||||
cmake --build "$BUILD_DIR" -j
|
||||
|
||||
# 3. compress
|
||||
WASM="$BUILD_DIR/$APP.wasm"
|
||||
gzip -9 -k "$WASM"
|
||||
brotli -q 11 -k "$WASM"
|
||||
|
||||
# 4. report
|
||||
echo "── Sizes ──"
|
||||
ls -lah "$BUILD_DIR/$APP".{wasm,wasm.gz,wasm.br,js,html}
|
||||
|
||||
# 5. budget check
|
||||
if [ "${1:-}" != "--no-budget-check" ]; then
|
||||
bash bash/functions/gamedev/wasm_size_budget_check.sh "$BUILD_DIR" "$APP"
|
||||
fi
|
||||
```
|
||||
|
||||
## Funcion de auditoria
|
||||
|
||||
`bash/functions/gamedev/wasm_size_budget_check.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Falla con exit 1 si excede el budget duro
|
||||
WASM_GZ="$BUILD_DIR/$APP.wasm.gz"
|
||||
SIZE=$(stat -c%s "$WASM_GZ")
|
||||
LIMIT=$((2 * 1024 * 1024))
|
||||
if [ "$SIZE" -gt "$LIMIT" ]; then
|
||||
echo "❌ $WASM_GZ = $SIZE bytes > $LIMIT (2 MB)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ $WASM_GZ within budget"
|
||||
```
|
||||
|
||||
## Banderas de compilacion (MinSizeRel)
|
||||
|
||||
```cmake
|
||||
if(EMSCRIPTEN)
|
||||
target_compile_options(<target> PRIVATE
|
||||
-Os # Optimize for size
|
||||
-fno-exceptions # No C++ exceptions (huge win)
|
||||
-fno-rtti # No RTTI
|
||||
-ffunction-sections
|
||||
-fdata-sections
|
||||
)
|
||||
target_link_options(<target> PRIVATE
|
||||
-Os
|
||||
-sASSERTIONS=0 # No runtime assertions in release
|
||||
-sFILESYSTEM=0 # No emscripten filesystem (use fetch)
|
||||
-sMINIMAL_RUNTIME=2 # Slim JS runtime
|
||||
-sENVIRONMENT=web # Browser only, no node
|
||||
-sUSE_WEBGL2=1
|
||||
-sFULL_ES3=1
|
||||
-sALLOW_MEMORY_GROWTH=1
|
||||
-sINITIAL_MEMORY=33554432
|
||||
-sSTACK_SIZE=1048576
|
||||
--closure=1 # Closure compiler on JS glue
|
||||
-Wl,--gc-sections
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
`MINIMAL_RUNTIME=2` ahorra ~50KB de JS glue. `FILESYSTEM=0` ahorra otros ~30KB. `--closure=1` reduce JS otro ~30%.
|
||||
|
||||
## Herramientas de auditoria
|
||||
|
||||
Cuando el budget se rompe, herramientas para encontrar el culpable:
|
||||
|
||||
1. **`twiggy`** (Rust tool, MIT) — analiza wasm y lista funciones por tamaño:
|
||||
```bash
|
||||
twiggy top -n 50 build/wasm/engine_smoke.wasm
|
||||
```
|
||||
2. **`wasm-objdump -h`** — secciones del binario.
|
||||
3. **`emcc --emit-symbol-map`** — mapa de simbolos para correlacionar con codigo C++.
|
||||
4. **Bloaty** (Google, ASL2) — analisis size attribution.
|
||||
|
||||
Funcion: `bash/functions/gamedev/wasm_size_audit.sh <wasm_path>` — corre las 4 y produce reporte markdown.
|
||||
|
||||
## Tecnicas de reduccion (cuando se exceda)
|
||||
|
||||
Documentar en `cpp/GAMEDEV.md`:
|
||||
|
||||
1. **No usar `<iostream>`** — ~80KB de runtime. Usar `printf` o nada.
|
||||
2. **No usar `<filesystem>`** — pesado. SDL_RWops o emscripten fetch.
|
||||
3. **`std::function` → punteros a funcion** cuando se pueda. ~5KB por instancia.
|
||||
4. **`std::regex` prohibido** — ~100KB. Usar parsers manuales o `re2` mini.
|
||||
5. **STL containers** — usar pero con `-fno-exceptions`. `std::vector` OK, `std::map` evitar (preferir `std::vector<pair>` ordenado o `flat_hash_map` mini).
|
||||
6. **Templates** — instanciar pocas veces. Cada nueva instanciacion es codigo nuevo.
|
||||
7. **Inlining** — `-Os` ya lo balancea. No forzar `__forceinline`.
|
||||
8. **Vendor libs** — auditar antes de añadir. Cada lib pasa por size review en su PR.
|
||||
|
||||
## Streaming + caching
|
||||
|
||||
Para juegos mas grandes en el futuro:
|
||||
- `--preload-file` empotra assets en `.data` adyacente al wasm. Cargado de golpe.
|
||||
- `fetch()` async para assets opcionales (niveles avanzados, musica).
|
||||
- Service Worker para cache offline (en sub-issue futuro, no urgente).
|
||||
|
||||
## Integracion con CI
|
||||
|
||||
GitHub Actions / Gitea Actions workflow `gamedev-wasm-budget.yml`:
|
||||
- Trigger: PR que toca `cpp/apps/engine_*` o `cpp/functions/gfx/sg_*`
|
||||
- Build WASM
|
||||
- Comprueba budgets
|
||||
- Comenta tamaños en el PR
|
||||
- Falla si rompe limite duro
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] Pipeline `build_wasm_cpp_pipelines.sh` reproducible en Linux + Windows WSL.
|
||||
- [x] `engine_smoke` (de 0072a) pasa budget check.
|
||||
- [x] `wasm_size_audit.sh` produce reporte legible.
|
||||
- [x] CI bloquea PRs que rompen el budget.
|
||||
- [x] Documentacion `cpp/GAMEDEV.md` actualizada con reglas de tamaño.
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- WebGPU (sokol_gfx WebGPU backend es alpha) — diferido.
|
||||
- WASM SIMD — opcional, evaluar despues de medir hot paths.
|
||||
- Multi-threading WASM (SharedArrayBuffer + COOP/COEP headers) — overkill, lo dejamos para juegos que lo pidan.
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
id: 0072e
|
||||
title: gamedev — bridge crypto Web3 (wallets, sign tx) via JS interop en WASM
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, wasm, crypto, web3]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072a, 0072d]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Permitir que el juego (corriendo en WASM dentro de un navegador) interactue con wallets crypto del usuario (MetaMask, Phantom, Coinbase Wallet, WalletConnect) sin empotrar libs Web3 en el bundle WASM. Toda la logica Web3 vive en JS, el WASM la invoca via `EM_JS` / `EM_ASM`.
|
||||
|
||||
Razon: las libs Web3 nativas C++ son escasas, inmaduras y pesadas. JS tiene `ethers.js` (~120KB gz), `viem` (~50KB gz), `@solana/web3.js` (~60KB gz) maduras y testeadas. Cargar JS en navegador es gratis, empotrar C++ Web3 en wasm cuesta MB.
|
||||
|
||||
## Alcance
|
||||
|
||||
| Caso de uso | Soporte fase 1 |
|
||||
|---|---|
|
||||
| Conectar wallet (MetaMask, Phantom) | Si |
|
||||
| Leer direccion del usuario | Si |
|
||||
| Firmar mensaje (login passwordless) | Si |
|
||||
| Firmar transaccion (in-game payment) | Si |
|
||||
| Leer balance ETH/SOL/token | Si |
|
||||
| Mintear NFT | No (sub-issue 0072f) |
|
||||
| Listar NFTs del wallet (asset loading) | No (0072f) |
|
||||
| Eventos on-chain (leaderboards) | No (0072f) |
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ WASM (juego C++) │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ crypto_bridge.h │ │
|
||||
│ │ - bridge_connect_wallet() │ │
|
||||
│ │ - bridge_get_address() │ │
|
||||
│ │ - bridge_sign_message(msg) │ │
|
||||
│ │ - bridge_sign_tx(tx_json) │ │
|
||||
│ │ - bridge_get_balance() │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ ↕ EM_ASYNC_JS / EM_JS │
|
||||
└────────────────────────────────────────┘
|
||||
↕
|
||||
┌────────────────────────────────────────┐
|
||||
│ JS host (browser) │
|
||||
│ - ethers.js / viem │
|
||||
│ - @solana/web3.js │
|
||||
│ - WalletConnect SDK │
|
||||
│ - window.ethereum / window.solana │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
### En WASM (C++)
|
||||
|
||||
`cpp/functions/crypto/bridge_web3.{cpp,h,md}` (impure, WASM only):
|
||||
|
||||
```cpp
|
||||
namespace fn::crypto {
|
||||
|
||||
enum class Chain { Ethereum, Polygon, Arbitrum, Base, Solana };
|
||||
|
||||
struct ConnectResult {
|
||||
bool ok;
|
||||
std::string address;
|
||||
std::string chain_id;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
struct SignResult {
|
||||
bool ok;
|
||||
std::string signature; // hex 0x...
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// Async via Emscripten promises. Usa Asyncify o callbacks.
|
||||
void bridge_connect_wallet(Chain c, void (*cb)(const ConnectResult&, void*), void* user);
|
||||
void bridge_sign_message(const std::string& msg, void (*cb)(const SignResult&, void*), void* user);
|
||||
void bridge_sign_tx(const std::string& tx_json, void (*cb)(const SignResult&, void*), void* user);
|
||||
void bridge_get_balance(const std::string& token_addr, void (*cb)(uint64_t, void*), void* user);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Implementacion via `EM_ASYNC_JS`:
|
||||
|
||||
```cpp
|
||||
EM_ASYNC_JS(char*, js_connect_wallet, (int chain), {
|
||||
const result = await window.fnGameBridge.connectWallet(chain);
|
||||
return stringToNewUTF8(JSON.stringify(result));
|
||||
});
|
||||
```
|
||||
|
||||
### En JS host (no en registry, en `cpp/apps/<app>/web/`)
|
||||
|
||||
Cada app que use crypto incluye su `web/bridge.js` (compartible via plantilla):
|
||||
|
||||
```js
|
||||
// cpp/apps/engine_demo/web/bridge.js
|
||||
import { ethers } from "https://esm.sh/ethers@6";
|
||||
|
||||
window.fnGameBridge = {
|
||||
async connectWallet(chain) {
|
||||
if (!window.ethereum) return { ok: false, error: "no_provider" };
|
||||
try {
|
||||
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
|
||||
const chainId = await window.ethereum.request({ method: "eth_chainId" });
|
||||
return { ok: true, address: accounts[0], chain_id: chainId };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
},
|
||||
async signMessage(msg) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
try {
|
||||
const sig = await signer.signMessage(msg);
|
||||
return { ok: true, signature: sig };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
},
|
||||
async signTx(txJson) { /* ... */ },
|
||||
async getBalance(tokenAddr) { /* ... */ }
|
||||
};
|
||||
```
|
||||
|
||||
Tamaño JS: ethers.js gzip ~120KB. Cargado lazy (solo cuando el usuario abre menu wallet), no afecta el TTI inicial del juego.
|
||||
|
||||
## Asyncify vs JSPI
|
||||
|
||||
Emscripten ofrece dos formas de manejar async desde C++:
|
||||
|
||||
| Opcion | Pros | Contras |
|
||||
|---|---|---|
|
||||
| **Asyncify** | Funciona en todos los navegadores | +50-200KB al wasm, llamadas mas lentas |
|
||||
| **JSPI** (JS Promise Integration) | 0 overhead, tamaño minimo | Solo Chrome/Edge desde 2024, requiere flag |
|
||||
|
||||
Decision: **Asyncify** por compatibilidad. Permitir JSPI via build flag opcional para usuarios Chrome.
|
||||
|
||||
`-sASYNCIFY -sASYNCIFY_IMPORTS=['js_connect_wallet','js_sign_message',...]`
|
||||
|
||||
## Wallet support matrix
|
||||
|
||||
| Wallet | Chain | Mecanismo |
|
||||
|---|---|---|
|
||||
| MetaMask | EVM (ETH, Polygon, BSC, ...) | `window.ethereum` (EIP-1193) |
|
||||
| Coinbase Wallet | EVM | `window.ethereum` (mismo provider) |
|
||||
| Phantom | Solana | `window.solana` |
|
||||
| WalletConnect | EVM + Solana | SDK + QR / deep link (mobile) |
|
||||
| Trust Wallet (mobile) | EVM + Solana | WalletConnect |
|
||||
|
||||
Para mobile (Android/iOS) en sub-issue futuro: WalletConnect deep links + universal links.
|
||||
|
||||
## Seguridad
|
||||
|
||||
1. **Nunca empotrar private keys en WASM**. El juego solo PIDE firmas, nunca las tiene.
|
||||
2. **Mensaje a firmar SIEMPRE con dominio explicito** (EIP-191 personal_sign con prefijo o EIP-712 typed data) para evitar phishing cross-app.
|
||||
3. **Validar chain_id** antes de cada accion. Si user cambio de red, abortar.
|
||||
4. **Rate limit** de `bridge_sign_message` desde el juego: max 1 firma cada 5s para evitar spam UI.
|
||||
5. **Auditar JS host code** — es donde vive el riesgo. Versiones pinneadas de ethers/viem, no CDN sin SRI.
|
||||
|
||||
## Login passwordless con firma
|
||||
|
||||
Patron canonico para identificar al jugador sin password:
|
||||
|
||||
```
|
||||
1. Servidor genera nonce: "Sign this to login: <random_uuid>" + timestamp
|
||||
2. Cliente llama bridge_sign_message(nonce)
|
||||
3. Servidor verifica firma → recupera address
|
||||
4. Servidor emite JWT con sub=address
|
||||
```
|
||||
|
||||
Funcion auxiliar en registry: `crypto_verify_eth_signature_go_crypto` (ya existe? si no, crear) para el backend.
|
||||
|
||||
## Pago in-game
|
||||
|
||||
```
|
||||
1. Juego pide a JS: "send 0.01 ETH to <addr>"
|
||||
2. JS abre MetaMask popup con tx
|
||||
3. Usuario aprueba en wallet
|
||||
4. JS devuelve tx_hash al WASM
|
||||
5. WASM polleea balance / espera confirmacion via JS bridge
|
||||
```
|
||||
|
||||
## Tamaño
|
||||
|
||||
- Bridge C++: ~3 KB.
|
||||
- Asyncify overhead: ~80 KB wasm (cuando se activa).
|
||||
- JS host (ethers.js): ~120 KB gz, **lazy loaded** (no cuenta para TTI).
|
||||
|
||||
## Tests
|
||||
|
||||
- Mock de `window.fnGameBridge` para tests unit del bridge C++ (responde con respuestas predefinidas).
|
||||
- Test e2e en navegador (Playwright) que abre el juego con MetaMask en un wallet de prueba, firma un mensaje, valida la respuesta. Sub-issue 0072f probablemente.
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] Bridge C++ + JS funcionando en `engine_smoke` con boton "Connect Wallet".
|
||||
- [x] Firma de mensaje con MetaMask returna ok.
|
||||
- [x] Pago de 0.001 ETH en testnet (Sepolia) confirmado.
|
||||
- [x] Asyncify overhead ≤ 100 KB wasm.
|
||||
- [x] Documentacion `cpp/GAMEDEV.md` seccion crypto.
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Wallets nativos (todo via JS).
|
||||
- Soporte mobile profundo (en 0072g/0072h con WalletConnect).
|
||||
- NFTs / on-chain assets (0072f).
|
||||
- Backend de leaderboards firmado (0072f).
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
id: 0072f
|
||||
title: gamedev — crypto on-chain (NFT assets, payments, leaderboards firmadas)
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, crypto, web3, nft]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072e]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Construir las primitivas para juegos crypto-aware: cargar assets desde NFTs del wallet del jugador, gating de contenido por tenencia de tokens, pagos in-game on-chain, y leaderboards verificables (firmados, anti-cheat basico).
|
||||
|
||||
Toda la logica viaja por el bridge JS de 0072e + funciones backend nuevas.
|
||||
|
||||
## Casos de uso
|
||||
|
||||
| Caso | Descripcion |
|
||||
|---|---|
|
||||
| **NFT skin** | Player conecta wallet, juego carga PFP/skin desde NFT que posee. |
|
||||
| **Token gating** | Modos/niveles desbloqueados solo si wallet tiene cierto token o NFT. |
|
||||
| **In-game shop** | Pagos en stablecoin (USDC) por items. |
|
||||
| **Earn-to-play** | Pagos del juego al jugador (tx outgoing del game treasury). |
|
||||
| **Leaderboard firmada** | Score tagged con firma del jugador, servidor verifica autenticidad. |
|
||||
| **Cross-game inventory** | Inventario portable entre varios juegos del mismo studio (NFT estandar). |
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
### Lectura on-chain (en JS bridge, expuesto a WASM)
|
||||
|
||||
```js
|
||||
window.fnGameBridge.crypto = {
|
||||
async getNFTs(address, contractAddr) // ERC721/1155 listing
|
||||
async getTokenBalance(address, token) // ERC20 balance
|
||||
async getENS(address) // ENS reverse lookup
|
||||
async ownsNFT(address, contract, tokenId) // bool, gating
|
||||
};
|
||||
```
|
||||
|
||||
Implementacion: usar `viem` (~50KB gz) o llamadas RPC directas (`fetch` a Alchemy/Infura/Public RPC).
|
||||
|
||||
### En C++ (registry)
|
||||
|
||||
`cpp/functions/crypto/nft_loader.{cpp,h,md}` (impure, WASM):
|
||||
|
||||
```cpp
|
||||
struct NFTAsset {
|
||||
std::string contract;
|
||||
std::string token_id;
|
||||
std::string name;
|
||||
std::string image_url; // ipfs:// o https://
|
||||
std::string metadata_json; // raw
|
||||
};
|
||||
|
||||
void crypto_list_nfts(const std::string& address,
|
||||
void (*cb)(std::vector<NFTAsset>, void*), void* user);
|
||||
void crypto_load_nft_image(const NFTAsset& nft,
|
||||
void (*cb)(std::vector<uint8_t>, void*), void* user);
|
||||
```
|
||||
|
||||
`cpp/functions/crypto/token_gating.{cpp,h,md}` (impure, WASM):
|
||||
|
||||
```cpp
|
||||
struct GateRule {
|
||||
std::string contract;
|
||||
std::string token_id; // "" = any token of contract
|
||||
uint64_t min_balance;
|
||||
};
|
||||
|
||||
void crypto_check_gate(const std::string& address, const GateRule& rule,
|
||||
void (*cb)(bool ok), void* user);
|
||||
```
|
||||
|
||||
`cpp/functions/crypto/sign_score.{cpp,h,md}` (impure, WASM):
|
||||
|
||||
```cpp
|
||||
// Player firma su score → servidor verifica
|
||||
struct ScoreEntry {
|
||||
std::string game_id;
|
||||
std::string level;
|
||||
uint64_t score;
|
||||
uint64_t timestamp;
|
||||
std::string nonce;
|
||||
};
|
||||
void crypto_sign_score(const ScoreEntry& s,
|
||||
void (*cb)(std::string sig, void*), void* user);
|
||||
```
|
||||
|
||||
### Backend (verificacion de firmas)
|
||||
|
||||
Funciones Go nuevas en `functions/crypto/`:
|
||||
|
||||
- `eth_verify_signature_go_crypto` (pure) — recover signer address de firma + mensaje. Vendoring: `go-ethereum/crypto`.
|
||||
- `eip712_hash_go_crypto` (pure) — hash de typed data segun EIP-712.
|
||||
- `solana_verify_signature_go_crypto` (pure) — ed25519 verify.
|
||||
|
||||
Service nuevo en `apps/`: `crypto_leaderboard_api` (tag `service`):
|
||||
- POST `/score` con `ScoreEntry` + firma → verifica → guarda en BD si valido.
|
||||
- GET `/leaderboard?game=X&level=Y` → top scores firmados.
|
||||
- BD: SQLite con tabla `scores` (game_id, level, address, score, signature, timestamp).
|
||||
|
||||
### Frontend dashboard
|
||||
|
||||
`apps/crypto_leaderboard_api/web/` — pagina React/Mantine que consume la API y muestra leaderboards. Usa `@fn_library`.
|
||||
|
||||
## NFT image loading
|
||||
|
||||
Las imagenes de NFTs vienen via:
|
||||
- `ipfs://Qm...` — resolver con gateway publico (`https://ipfs.io/ipfs/`, `https://nftstorage.link`, `https://cloudflare-ipfs.com`).
|
||||
- `https://...` — directo.
|
||||
- `data:image/png;base64,...` — embebido.
|
||||
|
||||
Funcion: `crypto_resolve_ipfs_url_cpp_crypto` (pure):
|
||||
|
||||
```cpp
|
||||
std::string resolve_ipfs(const std::string& uri,
|
||||
const std::vector<std::string>& gateways = {});
|
||||
```
|
||||
|
||||
Failover entre gateways si uno falla.
|
||||
|
||||
## Cache local
|
||||
|
||||
NFTs raramente cambian. Cache en localStorage / IndexedDB:
|
||||
|
||||
```js
|
||||
window.fnGameBridge.cache = {
|
||||
async getNFT(contract, tokenId) { /* IndexedDB lookup */ },
|
||||
async putNFT(nft) { /* IndexedDB store */ },
|
||||
};
|
||||
```
|
||||
|
||||
C++ no toca cache directamente; el JS bridge gestiona cache con TTL 24h.
|
||||
|
||||
## Pagos in-game
|
||||
|
||||
Patron canonico:
|
||||
|
||||
```cpp
|
||||
// Comprar power-up por 1 USDC
|
||||
struct Payment {
|
||||
std::string token_contract; // USDC mainnet
|
||||
std::string to_address; // Game treasury
|
||||
std::string amount_wei; // 1000000 = 1 USDC (6 decimales)
|
||||
std::string memo; // "buy:powerup_001:player_123"
|
||||
};
|
||||
|
||||
void crypto_pay(const Payment& p, void (*cb)(std::string tx_hash, void*), void* user);
|
||||
```
|
||||
|
||||
Backend verifica via webhook / polling:
|
||||
1. Watch transactions on `to_address`.
|
||||
2. Match `memo` → entregar item al usuario.
|
||||
3. Servicio nuevo: `crypto_payment_watcher` (tag `service`).
|
||||
|
||||
## EIP-712 typed data
|
||||
|
||||
Para firmas estructuradas (mejor UX que `personal_sign` plano):
|
||||
|
||||
```js
|
||||
// JS bridge expone:
|
||||
async signTypedData(domain, types, message) {
|
||||
return await signer.signTypedData(domain, types, message);
|
||||
}
|
||||
```
|
||||
|
||||
Beneficio: MetaMask muestra el contenido legible al usuario, no un blob hex. Reduce phishing.
|
||||
|
||||
## Anti-cheat layers
|
||||
|
||||
Score firmado NO es anti-cheat completo (un cheater puede firmar scores falsos). Layers complementarios:
|
||||
|
||||
1. **Sanity bounds** — backend rechaza scores imposibles (ej. > max teorico).
|
||||
2. **Replay attestation** — guardar inputs del juego junto al score, replay deterministic en backend para validar.
|
||||
3. **Server-authoritative** para modos competitivos — el juego envia inputs, el server simula. No es para todos los juegos.
|
||||
4. **Rate limit** por wallet — max N scores/hora.
|
||||
|
||||
Documentar en `cpp/GAMEDEV.md` que la firma es **autenticacion**, no **integridad del score**.
|
||||
|
||||
## Integracion con apps existentes
|
||||
|
||||
Si una app C++ quiere features crypto:
|
||||
1. Declara `uses_functions: [bridge_web3_cpp_crypto, nft_loader_cpp_crypto, ...]`.
|
||||
2. Compila con `-sASYNCIFY` y los `ASYNCIFY_IMPORTS` del bridge.
|
||||
3. Embebe `web/bridge.js` + `web/crypto.js` en su `shell.html`.
|
||||
|
||||
## Tamaño
|
||||
|
||||
- C++ extra: ~10 KB total (bridge + nft loader + token gating + sign score).
|
||||
- JS host extra: ~50 KB gz (viem) + ~20 KB gz (helpers). **Lazy loaded**, no afecta TTI inicial salvo que el juego conecte wallet en startup (no recomendado).
|
||||
- Backend Go: parte de un service separado, no afecta runtime del juego.
|
||||
|
||||
## Standards a soportar
|
||||
|
||||
| Standard | Para qué |
|
||||
|---|---|
|
||||
| ERC-721 | NFT unicos (skins, characters) |
|
||||
| ERC-1155 | NFT semi-fungibles (items, currency) |
|
||||
| ERC-20 | Tokens fungibles (in-game currency) |
|
||||
| EIP-712 | Typed data signing |
|
||||
| EIP-1193 | Wallet provider interface |
|
||||
| Metaplex (Solana) | NFTs Solana |
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] `engine_demo` carga PFP NFT del wallet conectado.
|
||||
- [x] Token gating: solo wallets con NFT X pueden entrar al modo Y.
|
||||
- [x] Pago de 1 USDC testnet entrega un item in-game tras confirmacion.
|
||||
- [x] Leaderboard API valida firmas correctamente y rechaza falsificaciones.
|
||||
- [x] Tests: mock provider para validar flujos sin red real.
|
||||
- [x] Documentacion completa en `cpp/GAMEDEV.md` seccion crypto on-chain.
|
||||
|
||||
## Riesgos
|
||||
|
||||
1. **Gas fees** — pagos pequeños on-chain en mainnet ETH son inviables (gas > tx). Estrategia: L2 (Base, Arbitrum, Polygon) o Solana de default.
|
||||
2. **IPFS gateway flaky** — failover entre gateways, cache local.
|
||||
3. **Wallet UX en mobile** — WalletConnect deep links requieren testing real en devices.
|
||||
4. **Regulacion** — payments crypto pueden caer bajo MiCA (EU) o equivalentes. Documentar disclaimers, no consejo legal aqui.
|
||||
5. **Phishing** — siempre EIP-712 con domain explicito. Documentar para devs que usen el stack.
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Wallet integrado en el juego (custodial). Demasiado complejo + responsabilidad legal.
|
||||
- Smart contracts propios — issue separado por juego, no aplica al stack genérico.
|
||||
- Tokenomics / gobernanza — no es problema del runtime.
|
||||
@@ -0,0 +1,241 @@
|
||||
---
|
||||
id: 0072g
|
||||
title: gamedev — Android build (NDK + touch input + virtual gamepad + WalletConnect)
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, android, mobile]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072b, 0072c]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Compilar el runtime gamedev a APK Android, ejecutarlo en device fisico, gestionar touch input + virtual gamepad overlay, integrar wallet crypto via WalletConnect deep links, y mantener APK base ≤ 20 MB.
|
||||
|
||||
## Toolchain
|
||||
|
||||
Requisitos en host:
|
||||
- Android SDK (cmdline-tools)
|
||||
- Android NDK r27+ (LTS)
|
||||
- JDK 17
|
||||
- Gradle 8+
|
||||
- `adb` para deploy a device
|
||||
|
||||
Funcion bash: `bash/functions/infra/setup_android_ndk.sh` que descarga e instala SDK+NDK en `~/android-sdk/`. Idempotente.
|
||||
|
||||
## Estructura del proyecto Android
|
||||
|
||||
SDL3 ya provee plantilla Android. Adaptar:
|
||||
|
||||
```
|
||||
cpp/apps/<app>/android/
|
||||
build.gradle # Top-level
|
||||
settings.gradle
|
||||
app/
|
||||
build.gradle # App module (NDK config, ABI filters)
|
||||
AndroidManifest.xml
|
||||
src/main/
|
||||
java/com/.../MainActivity.java # Hereda SDL3 main activity
|
||||
jni/ # NDK wrapper
|
||||
Android.mk / CMakeLists.txt
|
||||
res/
|
||||
drawable/icon.png
|
||||
values/strings.xml
|
||||
```
|
||||
|
||||
## CMake cross-compile
|
||||
|
||||
```cmake
|
||||
# Ya en cpp/apps/<app>/CMakeLists.txt, sin cambios.
|
||||
# Build invocado desde Android Studio o gradle:
|
||||
# ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
ABIs target:
|
||||
- `arm64-v8a` — obligatorio (todos los devices modernos)
|
||||
- `armeabi-v7a` — opcional (devices viejos)
|
||||
- `x86_64` — solo para emulador
|
||||
|
||||
Cada ABI ≈ 5-7 MB del APK base. Distribuir como **App Bundle (AAB)** en Play Store, que entrega solo la ABI del device.
|
||||
|
||||
## Touch input
|
||||
|
||||
`cpp/functions/gamedev/input_unified` (de 0072b) ya tiene array `touches[8]`. Aqui lo poblamos.
|
||||
|
||||
SDL3 provee:
|
||||
```c
|
||||
SDL_FINGERDOWN, SDL_FINGERUP, SDL_FINGERMOTION
|
||||
```
|
||||
|
||||
`input_unified` mapea a `InputState.touches`. Cada touch tiene `id`, `x`, `y` (normalizado 0..1), `pressure`.
|
||||
|
||||
## Virtual gamepad
|
||||
|
||||
Funcion nueva: `virtual_gamepad_cpp_gamedev` (impure):
|
||||
|
||||
```cpp
|
||||
struct VGamepadCfg {
|
||||
bool show_dpad; // Lado izquierdo
|
||||
bool show_buttons; // Lado derecho (A, B, X, Y)
|
||||
float dpad_radius; // En pixels
|
||||
float button_radius;
|
||||
Color color_active;
|
||||
Color color_idle;
|
||||
};
|
||||
|
||||
void vgamepad_render(const VGamepadCfg& cfg, InputState& out_input,
|
||||
const InputState& touch_input);
|
||||
```
|
||||
|
||||
Logica:
|
||||
- Detecta touches dentro del area del dpad → setea `out_input.left/right/up/down`.
|
||||
- Detecta touches en area de botones → setea `out_input.action_a/b/x/y`.
|
||||
- Render: dibuja con `sprite_batch` los iconos del dpad y botones.
|
||||
|
||||
Estilo: semi-transparente, configurable color/tamaño. Sprites por defecto en `cpp/assets/vgamepad/` (dpad.png, btn_a.png, ...).
|
||||
|
||||
Auto-hide opcional cuando hay gamepad fisico conectado (Bluetooth):
|
||||
|
||||
```cpp
|
||||
if (SDL_GetGamepads(NULL) > 0) cfg.show_dpad = cfg.show_buttons = false;
|
||||
```
|
||||
|
||||
## Safe area / notch
|
||||
|
||||
Funcion: `safe_area_cpp_gamedev` (pure):
|
||||
|
||||
```cpp
|
||||
struct SafeArea { int top, bottom, left, right; }; // En pixels
|
||||
SafeArea safe_area_get(SDL_Window* w);
|
||||
```
|
||||
|
||||
SDL3: `SDL_GetWindowSafeArea(SDL_Window*, SDL_Rect*)`. Render UI dentro de ese rect.
|
||||
|
||||
## Permisos
|
||||
|
||||
`AndroidManifest.xml`:
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- WalletConnect deep link: -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="<app_name>" />
|
||||
</intent-filter>
|
||||
```
|
||||
|
||||
NO pedir permisos de:
|
||||
- Storage (escribir solo en `getFilesDir()`, no necesita permiso).
|
||||
- Camera/Mic — solo si el juego los usa.
|
||||
|
||||
## Crypto wallet (WalletConnect)
|
||||
|
||||
En mobile no hay `window.ethereum`. Usuario abre el juego, pulsa "Connect Wallet" → SDK genera URI WalletConnect → abre app de wallet (MetaMask Mobile, Trust, Rainbow) via deep link → wallet firma → callback con resultado.
|
||||
|
||||
Plan: empotrar **WalletConnect Sign SDK** version C (¿existe?). Si no:
|
||||
1. Implementar protocolo WalletConnect v2 minimo en C++ (WebSocket + JSON-RPC + AES-256-GCM). ~1000 LoC.
|
||||
2. Funcion: `walletconnect_session_cpp_crypto`, `walletconnect_request_cpp_crypto`.
|
||||
|
||||
Alternativa pragmatica: webview embebida con bridge JS (mismo patron que 0072e). Android tiene `WebView`, iOS tiene `WKWebView`. Pesa mas pero reusa codigo.
|
||||
|
||||
Decision pendiente — sub-issue dedicado al protocolo si se complica.
|
||||
|
||||
## Pipeline de build
|
||||
|
||||
`bash/functions/pipelines/build_android_cpp_pipelines.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
APP="$1"
|
||||
cd "cpp/apps/$APP/android"
|
||||
./gradlew assembleRelease
|
||||
APK="app/build/outputs/apk/release/app-release.apk"
|
||||
echo "APK: $(stat -c%s "$APK") bytes"
|
||||
```
|
||||
|
||||
Para deploy a device:
|
||||
```bash
|
||||
adb install -r app/build/outputs/apk/release/app-release.apk
|
||||
adb shell am start -n com.fn.<app>/.MainActivity
|
||||
```
|
||||
|
||||
## Firma del APK
|
||||
|
||||
Generar keystore una vez:
|
||||
```bash
|
||||
keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 \
|
||||
-validity 10000 -alias <app_name>
|
||||
```
|
||||
|
||||
Guardar en `~/keystores/` (gitignored) y referenciarlo en `gradle.properties`:
|
||||
```
|
||||
RELEASE_STORE_FILE=/home/lucas/keystores/<app>.keystore
|
||||
RELEASE_STORE_PASSWORD=...
|
||||
RELEASE_KEY_ALIAS=...
|
||||
RELEASE_KEY_PASSWORD=...
|
||||
```
|
||||
|
||||
## Tamaño
|
||||
|
||||
| Componente | Tamaño aprox |
|
||||
|---|---|
|
||||
| arm64-v8a `.so` (runtime) | 4-6 MB |
|
||||
| SDL3 lib | 1-2 MB |
|
||||
| Java glue | 50 KB |
|
||||
| Resources (icons) | 50 KB |
|
||||
| Assets bundle | depende del juego |
|
||||
|
||||
APK base objetivo: **≤ 20 MB** (sin assets pesados).
|
||||
|
||||
## Logging
|
||||
|
||||
`adb logcat | grep <app>` para ver `printf` y crashes. SDL3 redirige stdout/stderr a logcat por default.
|
||||
|
||||
Funcion: `bash/functions/infra/android_logcat_filter.sh <app_name>` que tail -f con filtros utiles.
|
||||
|
||||
## e2e_checks
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build_apk
|
||||
cmd: "bash bash/functions/pipelines/build_android_cpp_pipelines.sh <app>"
|
||||
timeout_s: 600
|
||||
- id: apk_size
|
||||
cmd: "test $(stat -c%s cpp/apps/<app>/android/app/build/outputs/apk/release/app-release.apk) -lt 20971520"
|
||||
- id: apk_install_emulator
|
||||
cmd: "bash bash/functions/infra/android_emulator_smoke.sh <app>"
|
||||
severity: warning # Emulador puede no estar disponible
|
||||
timeout_s: 300
|
||||
```
|
||||
|
||||
## Tests en device
|
||||
|
||||
Smoke test manual al principio. Automatizar despues con `adb shell input tap` + `adb shell screencap` + comparacion de imagenes (si vale la pena).
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] `engine_demo` corre en Android device (Pixel/Samsung de prueba).
|
||||
- [x] Touch input + virtual gamepad funcionan.
|
||||
- [x] Audio sin glitches.
|
||||
- [x] Safe area respetada (notch invisible no tapa UI).
|
||||
- [x] APK release ≤ 20 MB.
|
||||
- [x] WalletConnect: firma de mensaje funciona con MetaMask Mobile (basico, profundizar en sub-issue dedicado si hace falta).
|
||||
- [x] Pipeline reproducible desde Linux WSL + Windows.
|
||||
|
||||
## Riesgos
|
||||
|
||||
1. **WalletConnect en C++ puro** — protocolo no trivial. Plan B: webview.
|
||||
2. **NDK API levels** — minSdk 24 (Android 7). Devices < 7 fuera.
|
||||
3. **App Bundle requerido en Play Store** — `bundleRelease` en lugar de `assembleRelease` para production.
|
||||
4. **Apple-style audio latency** — Android variable. miniaudio AAudio backend ayuda en API 26+.
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Push notifications.
|
||||
- Google Play Billing — out of scope, prioridad crypto payments.
|
||||
- AdMob / ads.
|
||||
- iOS (en 0072h).
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
id: 0072h
|
||||
title: gamedev — iOS build (Xcode + Metal via sokol + safe area + WalletConnect)
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, ios, mobile]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072b, 0072c]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Compilar el runtime gamedev a IPA iOS, ejecutarlo en device (iPhone/iPad) usando Metal como backend grafico via sokol_gfx, gestionar touch input + virtual gamepad (compartido con Android), integrar WalletConnect para wallets crypto, y cumplir presupuesto IPA ≤ 25 MB.
|
||||
|
||||
## Requisitos duros
|
||||
|
||||
- **Mac fisico** o GitHub/Gitea Actions con runner `macos-latest`. NO se puede compilar iOS desde Linux/Windows.
|
||||
- **Apple Developer Program**: 99 USD/año para distribucion en App Store. Para test en device propio: cuenta gratuita basta (signing limited).
|
||||
- **Xcode 15+** instalado en el mac.
|
||||
- iPhone/iPad de prueba con iOS 15+.
|
||||
|
||||
## Toolchain
|
||||
|
||||
Si tenemos mac local: instalar Xcode + command line tools.
|
||||
|
||||
Si NO: GitHub Actions workflow (`gamedev-ios-build.yml`):
|
||||
```yaml
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: brew install cmake ninja
|
||||
- run: cmake -B build -S cpp/apps/<app> -GXcode -DCMAKE_SYSTEM_NAME=iOS
|
||||
- run: cmake --build build --config Release -- -sdk iphoneos
|
||||
- run: xcodebuild -project build/<app>.xcodeproj -scheme <app> archive
|
||||
# Upload artifact
|
||||
```
|
||||
|
||||
Coste: GitHub free tier no incluye macos minutes. Gitea Actions self-hosted en mac local es opcion si tenemos uno.
|
||||
|
||||
## CMake con Metal backend
|
||||
|
||||
```cmake
|
||||
if(IOS)
|
||||
target_compile_definitions(<target> PRIVATE SOKOL_METAL)
|
||||
target_link_libraries(<target> PRIVATE
|
||||
"-framework Metal"
|
||||
"-framework MetalKit"
|
||||
"-framework Foundation"
|
||||
"-framework UIKit"
|
||||
"-framework AVFoundation" # miniaudio
|
||||
"-framework AudioToolbox"
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
sokol_gfx tiene backend Metal nativo. Mismo codigo C++ del juego, sokol traduce las llamadas. Shaders: SPIRV-Cross convierte GLSL → MSL en build (sub-issue 0072c).
|
||||
|
||||
## Touch input
|
||||
|
||||
SDL3 ya tiene `SDL_FINGERDOWN/UP/MOTION` en iOS. `input_unified_cpp_gamedev` (de 0072b) los recibe igual que Android. Mismo `virtual_gamepad_cpp_gamedev` (de 0072g) sin cambios.
|
||||
|
||||
## Safe area
|
||||
|
||||
iOS notch + home indicator. SDL3: `SDL_GetWindowSafeArea`. Mismo helper `safe_area_cpp_gamedev` que Android.
|
||||
|
||||
## Info.plist
|
||||
|
||||
```xml
|
||||
<key>UILaunchScreen</key>
|
||||
<dict><key>UIColorName</key><string>LaunchScreenColor</string></dict>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<!-- WalletConnect deep link: -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array><string><app_name></string></array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- Universal links opcional: -->
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array><string>applinks:<app>.example.com</string></array>
|
||||
```
|
||||
|
||||
## Crypto wallet en iOS
|
||||
|
||||
Mismo plan que Android (sub-issue 0072g): WalletConnect deep links via Universal Links o custom URL schemes. Wallets compatibles: MetaMask iOS, Trust iOS, Rainbow, Coinbase Wallet.
|
||||
|
||||
Apple es estricto con apps que tocan crypto:
|
||||
- App Store Review Guideline **3.1.5 (b)** permite NFTs y blockchain features.
|
||||
- NO se puede usar IAP (in-app purchase) para comprar crypto/NFTs — debe ser tx on-chain directa.
|
||||
- NO ofrecer "rewards" en crypto sin disclaimers (3.1.1 anti-bypass).
|
||||
|
||||
Documentar en `cpp/GAMEDEV.md` seccion "iOS App Store compliance".
|
||||
|
||||
## Pipeline de build
|
||||
|
||||
`bash/functions/pipelines/build_ios_cpp_pipelines.sh` (corre solo en mac):
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
APP="$1"
|
||||
SRC="cpp/apps/$APP"
|
||||
BUILD="build/ios/$APP"
|
||||
|
||||
cmake -B "$BUILD" -S "$SRC" -GXcode \
|
||||
-DCMAKE_SYSTEM_NAME=iOS \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES=arm64
|
||||
|
||||
cmake --build "$BUILD" --config Release -- -sdk iphoneos
|
||||
# Archive + export
|
||||
xcodebuild -project "$BUILD/$APP.xcodeproj" \
|
||||
-scheme "$APP" -configuration Release \
|
||||
-archivePath "$BUILD/$APP.xcarchive" archive
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$BUILD/$APP.xcarchive" \
|
||||
-exportPath "$BUILD/ipa" \
|
||||
-exportOptionsPlist exportOptions.plist
|
||||
```
|
||||
|
||||
## Signing
|
||||
|
||||
`exportOptions.plist`:
|
||||
```xml
|
||||
<dict>
|
||||
<key>method</key><string>development</string> <!-- o app-store -->
|
||||
<key>teamID</key><string>XXXXXXXXXX</string>
|
||||
<key>signingStyle</key><string>automatic</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
Para CI: usar **fastlane match** o secrets de Apple Developer. Out of scope inicial — primer paso es hacer build local en mac.
|
||||
|
||||
## TestFlight / App Store distribucion
|
||||
|
||||
- TestFlight: subir IPA via `xcrun altool --upload-app` o `Transporter.app`. Beta tester invitations.
|
||||
- App Store: revision Apple ~24-72h tipica. Rejections crypto-related: documentar bien las features.
|
||||
|
||||
## Tamaño
|
||||
|
||||
iOS IPA es zip de `.app/`. Componentes:
|
||||
- Binario universal (solo arm64): 5-7 MB.
|
||||
- SDL3 framework: 2-3 MB.
|
||||
- Assets bundle: depende del juego.
|
||||
- App icon + launch screen: 100 KB.
|
||||
|
||||
IPA base objetivo: **≤ 25 MB**.
|
||||
|
||||
App Thinning en App Store reduce el download del usuario final (solo arm64, recursos por device).
|
||||
|
||||
## e2e_checks
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build_ios
|
||||
cmd: "bash bash/functions/pipelines/build_ios_cpp_pipelines.sh <app>"
|
||||
timeout_s: 900
|
||||
severity: warning # Solo corre en mac
|
||||
- id: ipa_size
|
||||
cmd: "test $(stat -c%s build/ios/<app>/ipa/<app>.ipa) -lt 26214400"
|
||||
severity: warning
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Manual primero (es lo que toca con iOS). Automatizar con XCUITest mas adelante si hay volumen de juegos.
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] `engine_demo` corre en iPhone fisico (test device).
|
||||
- [x] Metal backend activo (no rasterizer software).
|
||||
- [x] Touch + virtual gamepad funcionan.
|
||||
- [x] Audio sin glitches (CoreAudio via miniaudio).
|
||||
- [x] Safe area respetada en iPhone con notch.
|
||||
- [x] IPA ≤ 25 MB.
|
||||
- [x] WalletConnect deep link basico funciona con MetaMask iOS.
|
||||
- [x] Pipeline documentado para mac local + CI macos-latest.
|
||||
|
||||
## Riesgos
|
||||
|
||||
1. **Sin mac** — bloqueante. Resolver antes de empezar.
|
||||
2. **Apple Developer Program** — 99 USD/año fijo. Sin esto solo "personal device sideloading" (limitado).
|
||||
3. **App Store review crypto** — rechazos posibles. Tener un build "vanilla" sin crypto features para fallback.
|
||||
4. **MoltenVK alternativa** — si sokol_gfx Metal da problemas, MoltenVK (Vulkan→Metal) es opcion. +complejidad +tamaño. Plan B.
|
||||
5. **iOS 15+ minimum** — corta dispositivos pre-2015 (iPhone 6S y anteriores).
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- TestFlight automatizado (mas tarde).
|
||||
- Apple Pay para fiat → crypto.
|
||||
- Wallet propio custodial (no, mismo motivo que 0072e).
|
||||
- iPad-specific UI (escala automatica desde iPhone basta para empezar).
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
id: 0072i
|
||||
title: gamedev — editor visual `game_editor` (scene tree, asset browser, inspector)
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, cpp, editor, tooling]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072b, 0072c]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
App PC `cpp/apps/game_editor/` estilo `shaders_lab` / `chart_demo`: editor visual basado en ImGui que abre/edita/exporta proyectos de juego usando los formatos del runtime (sub-issues 0072b, 0072c). NO es Godot empotrado — es un editor minimalista construido con las funciones del registry, hot-reloadable, integrado en el ecosistema fn.
|
||||
|
||||
## Filosofia
|
||||
|
||||
| Concepto | Decision |
|
||||
|---|---|
|
||||
| Visual scripting | NO — todo en C++ con hot reload (dylib opcional) |
|
||||
| Scene format | JSON o binario (mismo `.pak` del runtime) |
|
||||
| Hot reload assets | Si — file watcher + reload en runtime conectado |
|
||||
| Live preview | Si — el editor lanza el runtime como subprocess y se comunica via IPC |
|
||||
| Multi-platform editor | NO — solo PC. Mobile no edita, solo ejecuta. |
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Menu: File · Edit · View · Build · Help │
|
||||
├─────────┬───────────────────────────────────────┬───────────┤
|
||||
│ Scene │ │ Inspector │
|
||||
│ Tree │ │ │
|
||||
│ │ Scene Viewport │ - Pos │
|
||||
│ - Root │ (live runtime via IPC) │ - Sprite │
|
||||
│ - Bg │ │ - Script │
|
||||
│ - Pl │ │ ... │
|
||||
│ - En │ │ │
|
||||
├─────────┼───────────────────────────────────────┼───────────┤
|
||||
│ Assets │ │ Layers │
|
||||
│ Browser │ │ Anim │
|
||||
│ │ │ Timeline │
|
||||
└─────────┴───────────────────────────────────────┴───────────┘
|
||||
```
|
||||
|
||||
Implementado con `AppShell` style + ImGui dockspace + paneles del registry.
|
||||
|
||||
## Paneles a crear
|
||||
|
||||
### Scene tree
|
||||
|
||||
`cpp/functions/gamedev/scene_tree_panel.{cpp,h,md}` (impure):
|
||||
|
||||
```cpp
|
||||
struct SceneNode {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string type; // "sprite", "tilemap", "particle", "audio", "trigger"
|
||||
Vec2 pos, scale;
|
||||
float rotation;
|
||||
std::vector<std::string> children;
|
||||
nlohmann::json props; // Type-specific data
|
||||
};
|
||||
|
||||
void scene_tree_render(SceneTreeState& s, std::vector<SceneNode>& nodes,
|
||||
std::string& selected);
|
||||
```
|
||||
|
||||
Drag-reparent, multi-select, search, lock/visibility per node.
|
||||
|
||||
### Inspector
|
||||
|
||||
`cpp/functions/gamedev/inspector_panel.{cpp,h,md}` (impure):
|
||||
|
||||
Inspecciona el nodo seleccionado, muestra sus props en widgets ImGui, escribe cambios en el modelo. Genera UI desde un schema JSON declarado por tipo de nodo:
|
||||
|
||||
```json
|
||||
{
|
||||
"sprite": [
|
||||
{ "name": "atlas", "type": "asset_ref", "asset": "atlas" },
|
||||
{ "name": "frame", "type": "string" },
|
||||
{ "name": "tint", "type": "color" },
|
||||
{ "name": "z_order", "type": "int", "min": -100, "max": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Reusable: el mismo panel sirve para inspeccionar entities en otras apps. Generaliza `params_schema` del registry.
|
||||
|
||||
### Asset browser
|
||||
|
||||
`cpp/functions/gamedev/asset_browser_panel.{cpp,h,md}` (impure):
|
||||
|
||||
Grid de thumbnails. Filtrable por tipo (sprite, sound, shader, script). Drag a la viewport o al inspector.
|
||||
|
||||
Backend: directorio del proyecto + watcher (`fn::file_watcher` ya existe en registry?).
|
||||
|
||||
### Tilemap editor
|
||||
|
||||
`cpp/functions/gamedev/tilemap_editor_panel.{cpp,h,md}` (impure):
|
||||
|
||||
Edita layers de tilemap. Brush painting, eraser, fill bucket, picker. Edita formato compatible con `tilemap_compile_cpp_gfx` (de 0072c).
|
||||
|
||||
### Animation timeline
|
||||
|
||||
`cpp/functions/gamedev/anim_timeline_panel.{cpp,h,md}` (impure):
|
||||
|
||||
Reusa `cpp/functions/.../animation_curves` (ya existe del issue 0031). Crea state machines simples: idle/walk/jump por sprite.
|
||||
|
||||
### Shader graph
|
||||
|
||||
Reusa `cpp/functions/gfx/dag_*` (ya existe). Editor visual de shaders 2D que produce GLSL ES 300.
|
||||
|
||||
## Live preview via IPC
|
||||
|
||||
El editor lanza el runtime como subprocess en una ventana separada y le envia comandos via stdin/stdout JSON-RPC (o socket local):
|
||||
|
||||
```
|
||||
editor → runtime: { "method": "load_scene", "params": { "path": "/tmp/preview.json" } }
|
||||
runtime → editor: { "result": { "ok": true } }
|
||||
|
||||
editor → runtime: { "method": "select_node", "params": { "id": "player" } }
|
||||
editor → runtime: { "method": "set_prop", "params": { "id": "player", "key": "tint", "value": [1,0,0,1] } }
|
||||
```
|
||||
|
||||
El runtime, al recibir un cambio, repinta el frame. Usuario ve cambios al instante.
|
||||
|
||||
Funciones nuevas:
|
||||
- `cpp/functions/core/json_rpc_server.{cpp,h,md}` (impure)
|
||||
- `cpp/functions/core/json_rpc_client.{cpp,h,md}` (impure)
|
||||
|
||||
Si ya existe algo en el registry para IPC, reusarlo. Buscar en FTS5.
|
||||
|
||||
## Project format
|
||||
|
||||
```
|
||||
my_game/
|
||||
project.json # Metadata, scenes, build targets
|
||||
scenes/
|
||||
main_menu.json
|
||||
level_1.json
|
||||
assets/
|
||||
sprites/
|
||||
player.png
|
||||
enemy.png
|
||||
sounds/
|
||||
jump.wav
|
||||
fonts/
|
||||
Roboto.ttf
|
||||
shaders/
|
||||
bg_gradient.glsl
|
||||
scripts/ # Si hay scripting (futuro)
|
||||
build/ # Output del asset_compiler + binarios
|
||||
```
|
||||
|
||||
`project.json` schema documentado en `cpp/GAMEDEV.md`.
|
||||
|
||||
## Build button
|
||||
|
||||
Toolbar tiene botones:
|
||||
- **Build PC** → invoca `build_pc_cpp_pipelines.sh` para la app objetivo
|
||||
- **Build WASM** → invoca `build_wasm_cpp_pipelines.sh` (issue 0072d)
|
||||
- **Build Android** → invoca `build_android_cpp_pipelines.sh` (issue 0072g)
|
||||
- **Build iOS** → muestra mensaje "Build iOS requires mac" si no estamos en mac (0072h)
|
||||
|
||||
Output del build se streamea en un panel de log integrado.
|
||||
|
||||
## Persistencia
|
||||
|
||||
- `project.json` — formato del proyecto, versionado en git por el usuario.
|
||||
- `~/.fn_game_editor/recent.json` — proyectos recientes.
|
||||
- `<exe_dir>/local_files/editor_settings.json` — settings del editor (paneles abiertos, layout, etc.). Reusa convencion `fn::local_path` (ver `cpp_apps.md`).
|
||||
|
||||
## e2e_checks
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build
|
||||
cmd: "cmake --build build --target game_editor -j"
|
||||
- id: self_test
|
||||
cmd: "./build/cpp/apps/game_editor/game_editor --self-test"
|
||||
timeout_s: 30
|
||||
- id: open_sample_project
|
||||
cmd: "./build/cpp/apps/game_editor/game_editor --open samples/platformer/project.json --headless --quit-after 2s"
|
||||
timeout_s: 30
|
||||
```
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
cpp/apps/game_editor/
|
||||
CMakeLists.txt
|
||||
app.md
|
||||
main.cpp
|
||||
data.{cpp,h} # Project / Scene CRUD
|
||||
views.{cpp,h} # Composicion de paneles
|
||||
ipc.{cpp,h} # JSON-RPC con runtime
|
||||
samples/
|
||||
platformer/ # Proyecto demo
|
||||
project.json
|
||||
scenes/...
|
||||
```
|
||||
|
||||
## Reusabilidad
|
||||
|
||||
Muchos paneles (`scene_tree_panel`, `inspector_panel`, `asset_browser_panel`) son utiles fuera de gamedev. Otras apps del registry (`graph_explorer`, `kanban`) podrian reusarlos. Diseñarlos parametrizables desde el dia 1.
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] `game_editor` abre, lista escenas, edita un sprite, exporta y se ve el cambio en runtime.
|
||||
- [x] Live preview con IPC funciona (latencia < 100ms).
|
||||
- [x] Hot reload de assets cuando cambian en disco.
|
||||
- [x] Build PC + WASM desde el editor (botones funcionando).
|
||||
- [x] Sample project `platformer` jugable end-to-end.
|
||||
- [x] Paneles documentados como funciones del registry, reusables.
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Editor mobile-friendly. Solo PC.
|
||||
- Visual scripting. Codigo en C++ + scripting opcional (0072l).
|
||||
- Multi-user collab.
|
||||
- VCS integrado — usuario usa git fuera del editor.
|
||||
- Asset store / marketplace.
|
||||
|
||||
## Riesgos
|
||||
|
||||
1. **Inspector schema-driven** — abstraccion potencialmente sobreingenierizada. Empezar concreto, abstraer cuando haya 3 tipos de nodos.
|
||||
2. **IPC latency** — JSON-RPC sobre stdin/stdout puede tener overhead. Si pasa, evaluar shared memory.
|
||||
3. **Tilemap editor UX** — no es trivial. Reservar tiempo. Reusar lo que se pueda de Tiled (formato `.tmx`).
|
||||
@@ -0,0 +1,223 @@
|
||||
---
|
||||
id: 0072j
|
||||
title: gamedev — physics 2D (Box2D integration + funciones registry)
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, cpp, physics]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072b]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Integrar Box2D (v3, MIT, ~200KB) como motor de fisica 2D, expuesto via funciones del registry. Cubre colisiones, gravedad, joints, raycasts, sensores. Suficiente para plataformeros, top-down shooters, puzzle games con fisica, juegos tipo Angry Birds.
|
||||
|
||||
## Por qué Box2D v3
|
||||
|
||||
- MIT, sin restricciones.
|
||||
- ~200 KB strippable a menos.
|
||||
- C-API en v3 (mas facil de wrappear que la v2 C++ API).
|
||||
- Determinista (importante para replays / leaderboards firmados de 0072f).
|
||||
- Probado en miles de juegos.
|
||||
|
||||
Alternativas descartadas:
|
||||
- **Chipmunk2D** — bueno pero menos activo.
|
||||
- **Rapier** — Rust, complica integracion.
|
||||
- **PhysX** — overkill para 2D, licencia.
|
||||
- **Custom** — no, demasiado trabajo.
|
||||
|
||||
## Vendoring
|
||||
|
||||
`cpp/vendor/box2d/` con headers + `.c` source. Compilado como subdir del cmake de cada app que lo use, NO como funcion del registry (es vendor lib, no nuestro codigo).
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
`cpp/functions/gamedev/physics_*` (impure):
|
||||
|
||||
### physics_world
|
||||
|
||||
```cpp
|
||||
struct PhysicsWorld {
|
||||
b2WorldId world;
|
||||
Vec2 gravity;
|
||||
float time_step; // 1/60 default
|
||||
int substeps; // 4 default
|
||||
};
|
||||
|
||||
PhysicsWorld physics_world_create(Vec2 gravity);
|
||||
void physics_world_step(PhysicsWorld& w, float dt);
|
||||
void physics_world_destroy(PhysicsWorld& w);
|
||||
```
|
||||
|
||||
### physics_body
|
||||
|
||||
```cpp
|
||||
enum class BodyType { Static, Dynamic, Kinematic };
|
||||
|
||||
struct BodyDef {
|
||||
BodyType type;
|
||||
Vec2 position;
|
||||
float rotation;
|
||||
float linear_damping;
|
||||
float angular_damping;
|
||||
bool fixed_rotation;
|
||||
void* user_data;
|
||||
};
|
||||
|
||||
b2BodyId physics_body_create(PhysicsWorld& w, const BodyDef& def);
|
||||
void physics_body_destroy(b2BodyId id);
|
||||
void physics_body_set_velocity(b2BodyId id, Vec2 v);
|
||||
Vec2 physics_body_get_position(b2BodyId id);
|
||||
float physics_body_get_rotation(b2BodyId id);
|
||||
void physics_body_apply_impulse(b2BodyId id, Vec2 impulse);
|
||||
```
|
||||
|
||||
### physics_shape
|
||||
|
||||
```cpp
|
||||
struct ShapeDef {
|
||||
float density;
|
||||
float friction;
|
||||
float restitution; // bounciness 0..1
|
||||
bool is_sensor;
|
||||
uint16_t category_bits;
|
||||
uint16_t mask_bits;
|
||||
};
|
||||
|
||||
void physics_shape_box(b2BodyId body, Vec2 size, Vec2 center, const ShapeDef& def);
|
||||
void physics_shape_circle(b2BodyId body, float radius, Vec2 center, const ShapeDef& def);
|
||||
void physics_shape_polygon(b2BodyId body, const std::vector<Vec2>& verts, const ShapeDef& def);
|
||||
void physics_shape_chain(b2BodyId body, const std::vector<Vec2>& verts, bool loop);
|
||||
```
|
||||
|
||||
### physics_query
|
||||
|
||||
```cpp
|
||||
struct RaycastHit {
|
||||
b2BodyId body;
|
||||
Vec2 point;
|
||||
Vec2 normal;
|
||||
float fraction;
|
||||
bool hit;
|
||||
};
|
||||
|
||||
RaycastHit physics_raycast(PhysicsWorld& w, Vec2 from, Vec2 to,
|
||||
uint16_t mask = 0xFFFF);
|
||||
|
||||
std::vector<b2BodyId> physics_query_aabb(PhysicsWorld& w, Vec2 min, Vec2 max);
|
||||
|
||||
bool physics_overlap_circle(PhysicsWorld& w, Vec2 center, float radius,
|
||||
std::vector<b2BodyId>& out_bodies);
|
||||
```
|
||||
|
||||
### physics_contacts
|
||||
|
||||
```cpp
|
||||
struct ContactEvent {
|
||||
b2BodyId a, b;
|
||||
Vec2 point;
|
||||
Vec2 normal;
|
||||
float impulse;
|
||||
};
|
||||
|
||||
// Llamar despues de world_step
|
||||
std::vector<ContactEvent> physics_get_begin_contacts(PhysicsWorld& w);
|
||||
std::vector<ContactEvent> physics_get_end_contacts(PhysicsWorld& w);
|
||||
std::vector<ContactEvent> physics_get_sensor_events(PhysicsWorld& w);
|
||||
```
|
||||
|
||||
### physics_joints
|
||||
|
||||
```cpp
|
||||
b2JointId physics_joint_revolute(PhysicsWorld& w, b2BodyId a, b2BodyId b, Vec2 anchor);
|
||||
b2JointId physics_joint_distance(PhysicsWorld& w, b2BodyId a, b2BodyId b,
|
||||
Vec2 anchor_a, Vec2 anchor_b, float length);
|
||||
b2JointId physics_joint_prismatic(PhysicsWorld& w, b2BodyId a, b2BodyId b,
|
||||
Vec2 anchor, Vec2 axis);
|
||||
void physics_joint_destroy(b2JointId id);
|
||||
```
|
||||
|
||||
### physics_debug_draw
|
||||
|
||||
```cpp
|
||||
// Pinta shapes/aabb/contacts usando sprite_batch o lineas con sokol_gfx
|
||||
void physics_debug_draw(PhysicsWorld& w, SpriteBatch& batch, const Camera2D& cam,
|
||||
bool draw_shapes = true,
|
||||
bool draw_aabbs = false,
|
||||
bool draw_contacts = false);
|
||||
```
|
||||
|
||||
Util para debugging. No usar en release.
|
||||
|
||||
## Tipos del registry
|
||||
|
||||
`cpp/types/gamedev/`:
|
||||
- `BodyType` (sum: Static | Dynamic | Kinematic)
|
||||
- `BodyDef` (product)
|
||||
- `ShapeDef` (product)
|
||||
- `RaycastHit` (product)
|
||||
- `ContactEvent` (product)
|
||||
|
||||
`b2BodyId`, `b2JointId`, `b2WorldId` son types opacos del vendor; documentarlos como tales en el `.md` correspondiente.
|
||||
|
||||
## Integracion con runtime
|
||||
|
||||
`game_loop_cpp_gamedev` (de 0072b) ya tiene `on_fixed_update(dt)`. Ahi se llama `physics_world_step`. El render interpola entre dos snapshots de body positions (ya soportado por el game loop con `interp` factor).
|
||||
|
||||
## Patrones documentados en GAMEDEV.md
|
||||
|
||||
| Patron | Cuando |
|
||||
|---|---|
|
||||
| Static body + chain shape | Ground/walls de tilemap |
|
||||
| Dynamic body + box shape | Player, enemies |
|
||||
| Sensor (no-collide trigger) | Coins, checkpoints, damage zones |
|
||||
| Kinematic body | Plataformas moviles |
|
||||
| Raycast | Line of sight, bullets |
|
||||
| AABB query | Spatial culling, area-of-effect |
|
||||
| Categorias y masks | Player vs enemy vs wall vs trigger filtering |
|
||||
|
||||
## Determinismo
|
||||
|
||||
Box2D v3 es determinista si:
|
||||
1. Fixed timestep (no variable dt).
|
||||
2. Mismas operaciones en mismo orden.
|
||||
3. Misma version del compilador (con cuidado de `-ffast-math` que rompe determinism).
|
||||
|
||||
Para replays / leaderboards firmados (0072f): documentar que el game loop usa `fixed_dt` y `physics_world_step` siempre con ese dt. Inputs grabados → replay determinista.
|
||||
|
||||
## Tamaño
|
||||
|
||||
| Componente | KB |
|
||||
|---|---|
|
||||
| Box2D v3 stripped | ~200 |
|
||||
| Wrappers (registry funcs) | ~30 |
|
||||
| Total | ~230 |
|
||||
|
||||
Cabe en el budget global.
|
||||
|
||||
## Tests
|
||||
|
||||
App `cpp/apps/physics_test/` con `--self-test`:
|
||||
1. Crea world, body, shape.
|
||||
2. Step 100 frames.
|
||||
3. Verifica que el body cae (gravity).
|
||||
4. Verifica raycast contra un static body.
|
||||
5. Verifica contact event tras colision.
|
||||
6. Verifica determinismo: dos worlds idénticos producen mismas posiciones tras N steps.
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] Funciones registradas en registry con `.md` + tests.
|
||||
- [x] App `physics_test --self-test` pasa.
|
||||
- [x] Demo: caja cae sobre suelo, rebota, se detiene (en `engine_demo` de 0072k).
|
||||
- [x] Debug draw funcional (toggle en menu del editor).
|
||||
- [x] Tamaño contribuye ≤ 250 KB al wasm gzip.
|
||||
- [x] Documentacion `cpp/GAMEDEV.md` seccion Physics.
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Physics 3D (no, este stack es 2D).
|
||||
- GPU physics (overkill).
|
||||
- Soft body / cloth / fluids (Box2D no lo hace, OK).
|
||||
- Networking deterministic rollback (sub-issue futuro si hace falta multiplayer).
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
id: 0072k
|
||||
title: gamedev — demo plataformero `engine_demo` (referencia stack completo)
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, cpp, demo]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072b, 0072c, 0072d, 0072j]
|
||||
related_issues: [0072e, 0072f, 0072g, 0072h]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
App `cpp/apps/engine_demo/` que valida end-to-end que el stack funciona en las 4 plataformas: PC, Web, Android, iOS. Plataformero 2D simple, 1 nivel, con todas las features tipicas para servir como referencia y test de regresion del stack.
|
||||
|
||||
## Por qué este demo
|
||||
|
||||
- Sirve de test integrado del stack completo (runtime + assets + physics + crypto).
|
||||
- Es la referencia que cualquier dev nuevo lee para entender como ensamblar las funciones del registry en un juego completo.
|
||||
- Permite medir performance real (FPS, peso de bundle) en cada plataforma.
|
||||
- Acta como gate del CI: si `engine_demo` rompe, algo del stack rompio.
|
||||
|
||||
## Features del juego
|
||||
|
||||
| Feature | Funciones del registry usadas |
|
||||
|---|---|
|
||||
| Player con sprite animado | `sprite_batch`, `anim_*` (issue 0031), `input_unified` |
|
||||
| Movimiento (left/right/jump) | `physics_body`, `physics_shape`, `input_unified` |
|
||||
| Tilemap nivel 1 | `tilemap_render`, `tilemap_compile`, `physics_shape_chain` |
|
||||
| Coins coleccionables | sensores Box2D + `physics_get_sensor_events` |
|
||||
| Enemy patrullando | kinematic body + simple AI state machine |
|
||||
| Trampas (spikes) | sensor + game over |
|
||||
| Goal flag | sensor + level complete |
|
||||
| HUD: score + lives | ImGui o custom MSDF text |
|
||||
| Menu principal | ImGui o sprites + input |
|
||||
| Pause menu | ImGui |
|
||||
| Sound effects | `audio_play_sound` (jump, coin pickup, hit, win) |
|
||||
| Background music | `audio_play_music` |
|
||||
| Shaders vistosos | bg parallax + bloom post-process via `gfx/shader_canvas` |
|
||||
| Save game | local storage (PC) / localStorage (WASM) / `getFilesDir()` (Android/iOS) |
|
||||
| Settings (volumen, controles) | `app_settings` (ya en registry) |
|
||||
| Splash screen + loading | logo + barra |
|
||||
|
||||
Features crypto opcionales (gated por flag de build):
|
||||
- Connect wallet button (sub-issue 0072e)
|
||||
- High score leaderboard firmado (sub-issue 0072f)
|
||||
- NFT skin del wallet conectado (0072f)
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
cpp/apps/engine_demo/
|
||||
CMakeLists.txt
|
||||
app.md
|
||||
main.cpp # Entry point + game loop
|
||||
game.{cpp,h} # Game state machine (menu/play/pause/gameover)
|
||||
player.{cpp,h} # Player controller + animations
|
||||
enemy.{cpp,h} # Simple AI
|
||||
level.{cpp,h} # Tilemap loader + collision setup
|
||||
hud.{cpp,h} # Score, lives, timer
|
||||
menus.{cpp,h} # Title, pause, gameover, settings
|
||||
audio.{cpp,h} # SFX/music coordinator
|
||||
crypto.{cpp,h} # Wallet connect + leaderboard (#ifdef CRYPTO)
|
||||
android/ # Android project (sub-issue 0072g)
|
||||
ios/ # iOS project (sub-issue 0072h)
|
||||
web/ # WASM shell.html + bridge.js (sub-issue 0072e)
|
||||
assets/ # Source assets (.png, .wav, .ttf, .tmx)
|
||||
sprites/
|
||||
sounds/
|
||||
music/
|
||||
fonts/
|
||||
levels/
|
||||
build/ # Output del asset_compiler (gitignored)
|
||||
```
|
||||
|
||||
## Asset budget
|
||||
|
||||
Limites para mantener el download inicial razonable:
|
||||
|
||||
| Asset | Limite |
|
||||
|---|---|
|
||||
| Sprite atlas (1024x1024) | 200 KB PNG |
|
||||
| MSDF font | 50 KB PNG + 5 KB JSON |
|
||||
| SFX (4 sonidos, ogg) | 100 KB total |
|
||||
| Music (1 track, ogg vorbis q=3) | 1 MB |
|
||||
| Tilemap binario (1 nivel) | 30 KB |
|
||||
| Total assets bundle | ≤ 1.5 MB |
|
||||
|
||||
WASM gzip + assets gzip total: ≤ **3.5 MB** descargados.
|
||||
|
||||
## Pipeline de build
|
||||
|
||||
`bash/functions/pipelines/build_engine_demo_all.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 1. Compile assets
|
||||
./build/cpp/apps/asset_compiler/asset_compiler bundle \
|
||||
--in cpp/apps/engine_demo/assets \
|
||||
--out build/engine_demo/assets.pak
|
||||
|
||||
# 2. Build PC
|
||||
cmake --build build --target engine_demo -j
|
||||
|
||||
# 3. Build WASM
|
||||
bash bash/functions/pipelines/build_wasm_cpp_pipelines.sh engine_demo
|
||||
|
||||
# 4. Build Android (si hay NDK)
|
||||
[ -n "${ANDROID_NDK_HOME:-}" ] && \
|
||||
bash bash/functions/pipelines/build_android_cpp_pipelines.sh engine_demo
|
||||
|
||||
# 5. Build iOS (si hay mac)
|
||||
[ "$(uname)" = "Darwin" ] && \
|
||||
bash bash/functions/pipelines/build_ios_cpp_pipelines.sh engine_demo
|
||||
|
||||
# 6. Reporte
|
||||
echo "── Sizes ──"
|
||||
ls -lah build/engine_demo/
|
||||
```
|
||||
|
||||
## e2e_checks completos
|
||||
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build_pc
|
||||
cmd: "cmake --build build --target engine_demo -j"
|
||||
timeout_s: 300
|
||||
- id: self_test
|
||||
cmd: "./build/cpp/apps/engine_demo/engine_demo --self-test"
|
||||
timeout_s: 60
|
||||
- id: build_wasm
|
||||
cmd: "bash bash/functions/pipelines/build_wasm_cpp_pipelines.sh engine_demo"
|
||||
timeout_s: 600
|
||||
- id: wasm_size_budget
|
||||
cmd: "test $(stat -c%s build/wasm/engine_demo.wasm.gz) -lt 2097152" # 2MB
|
||||
- id: assets_size_budget
|
||||
cmd: "test $(stat -c%s build/engine_demo/assets.pak) -lt 1572864" # 1.5MB
|
||||
- id: replay_test
|
||||
cmd: "./build/cpp/apps/engine_demo/engine_demo --replay tests/replay_level1_complete.bin"
|
||||
timeout_s: 60
|
||||
- id: ops_audit
|
||||
ref: "fn-recopilador:apps/engine_demo"
|
||||
```
|
||||
|
||||
## Replay determinista
|
||||
|
||||
Para validar que el stack es determinista (importante por crypto leaderboards), grabar inputs de una run completa del nivel y poder reproducirla:
|
||||
|
||||
```cpp
|
||||
// --record output.bin → graba InputState cada frame
|
||||
// --replay input.bin → reproduce inputs, asserta que el final state es identico
|
||||
```
|
||||
|
||||
Funcion: `cpp/functions/gamedev/replay_record.{cpp,h,md}` + `replay_play.{cpp,h,md}`.
|
||||
|
||||
Si el replay diverge, hay un bug de determinismo en alguna funcion del stack. Es nuestro test de regresion mas potente.
|
||||
|
||||
## Niveles
|
||||
|
||||
1 solo nivel built-in para mantener simple. Si el editor (0072i) esta listo, el nivel se hace ahi y se exporta. Si no, se hace a mano en Tiled (formato `.tmx`).
|
||||
|
||||
## Visual style
|
||||
|
||||
Pixel art simple para mantener tamaño bajo. Paleta limitada (PICO-8 o similar). Background con gradiente shader animado (no PNG → ahorra KB).
|
||||
|
||||
Shaders incluidos:
|
||||
- Background gradient + parallax
|
||||
- Player squash/stretch on jump (vertex shader simple)
|
||||
- Coin sparkle (sprite + shader noise)
|
||||
- Bloom post-process (fullscreen pass) — opcional, toggle en settings
|
||||
- CRT-effect overlay — opcional, toggle en settings
|
||||
|
||||
## Crypto features (gated)
|
||||
|
||||
Build flag `CRYPTO=1` activa:
|
||||
- Boton "Connect Wallet" en main menu.
|
||||
- Leaderboard mostrando top 10 firmados.
|
||||
- Si conectado wallet con NFT skin, usa la imagen del NFT como sprite del player.
|
||||
|
||||
Sin `CRYPTO=1`, el juego es totalmente jugable sin wallet (importante: NO gating del juego basico).
|
||||
|
||||
## Criterio de exito
|
||||
|
||||
- [x] Jugable en PC (Win + Linux), navegador moderno (Chrome + Firefox), Android device, iOS device.
|
||||
- [x] Mismo binario base + plataforma layer especifica.
|
||||
- [x] Replay grabado en PC reproduce identico en WASM (test de determinismo).
|
||||
- [x] Tamaños dentro de budget en todas las plataformas.
|
||||
- [x] FPS estable ≥60 en hardware modesto.
|
||||
- [x] Crypto features funcionan en navegador con MetaMask.
|
||||
- [x] CI corre `e2e_checks` y bloquea regresiones.
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Multiple niveles. Solo 1 para demo.
|
||||
- Multiplayer.
|
||||
- Save/load con cloud sync.
|
||||
- Achievements / Steam integration.
|
||||
- Mas que un genero. Plataformero por encajar bien con todas las features.
|
||||
|
||||
## Riesgos
|
||||
|
||||
1. **Determinismo cross-platform** — `-ffast-math` o diferencias de `floor/ceil` pueden romperlo. Auditar flags por plataforma.
|
||||
2. **Audio latency mobile** — pruebas reales en device. Si > 80ms, evaluar AAudio backend en Android.
|
||||
3. **Performance en device viejo** — target Android 7+ y iPhone 6S+. Profile en hardware modesto.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
id: 0072l
|
||||
title: gamedev — scripting opcional (wren / lua / hot reload C++ dylib)
|
||||
status: deferred
|
||||
priority: low
|
||||
created: 2026-05-10
|
||||
tags: [gamedev, cpp, scripting]
|
||||
parent_issue: 0072
|
||||
depends_on: [0072k]
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Decidir si vale la pena añadir scripting al stack y, en su caso, integrarlo. Es un sub-issue **diferido**: solo se aborda cuando la fricción de iterar gameplay en C++ (recompilar) sea concretamente dolorosa, no antes.
|
||||
|
||||
## Cuando reconsiderar
|
||||
|
||||
Triggers para sacar este issue del modo `deferred`:
|
||||
- Recompilar `engine_demo` tras un cambio gameplay supera consistentemente 10s incluso con ccache + unity build.
|
||||
- Aparece un caso real de iterar gameplay en runtime sin reiniciar (designers no-developers tocando logica).
|
||||
- Aparece la necesidad de modding por usuarios finales.
|
||||
|
||||
Mientras eso no pasa: **no añadir scripting**. Cada lenguaje de scripting:
|
||||
- Suma ~50-200 KB al wasm.
|
||||
- Multiplica superficie de bugs (FFI bindings).
|
||||
- Confunde la cultura del registry (¿dónde vive el "codigo de juego"? ¿C++ o scripts?).
|
||||
|
||||
## Opciones
|
||||
|
||||
### Opcion A — wren
|
||||
|
||||
- Pequeño (~50 KB).
|
||||
- Sintaxis tipo Lua/Smalltalk.
|
||||
- API C limpia.
|
||||
- Single-author (Bob Nystrom), actualizaciones lentas.
|
||||
|
||||
### Opcion B — lua / luajit
|
||||
|
||||
- Lua estandar: ~200 KB. LuaJIT no compila a WASM, descartar.
|
||||
- Mas usuarios, mas docs.
|
||||
- Sintaxis familiar.
|
||||
- Bindings: sol2 (~5K LoC, pero header-only) o tolua manual.
|
||||
|
||||
### Opcion C — quickjs
|
||||
|
||||
- JS subset.
|
||||
- ~700 KB. Demasiado para nuestro budget.
|
||||
- Descartar a menos que necesitemos JS por compatibilidad con codigo crypto.
|
||||
|
||||
### Opcion D — hot reload C++ via dylib
|
||||
|
||||
- Compilar el codigo de juego como `.so` / `.dll` y recargar al cambiar.
|
||||
- 0 KB extra runtime.
|
||||
- No funciona en WASM (no hay dylib loading dinamico).
|
||||
- No funciona en iOS (no se permite carga dinamica de codigo).
|
||||
- Solo util en desktop dev workflow.
|
||||
|
||||
### Opcion E — sin scripting + ccache + unity build
|
||||
|
||||
- Compilar todo C++ con ccache. Cambios incrementales tipicos < 3s.
|
||||
- Unity build de la app reduce LTO time.
|
||||
- Hot reload de **assets y shaders** (que es lo que mas se itera) NO necesita scripting.
|
||||
|
||||
## Recomendacion previa
|
||||
|
||||
Empezar con Opcion E. Solo si el dolor real aparece, evaluar A (wren) por ser la mas pequeña y compatible WASM.
|
||||
|
||||
## Si se aborda: scope minimo
|
||||
|
||||
1. Scripting solo en niveles altos (gameplay logic, AI scripts, dialogos).
|
||||
2. Codigo "de motor" (rendering, physics, input) sigue en C++.
|
||||
3. Bindings expuestos via funciones del registry (`script_register_*` para cada subsistema).
|
||||
4. Hot reload de scripts en dev mode (file watcher).
|
||||
5. Empaquetar scripts en el `.pak` de assets.
|
||||
|
||||
## Funciones (cuando llegue el momento)
|
||||
|
||||
- `cpp/functions/gamedev/wren_vm.{cpp,h,md}` — VM lifecycle
|
||||
- `cpp/functions/gamedev/wren_bind.{cpp,h,md}` — registrar funciones C++ a wren
|
||||
- `cpp/functions/gamedev/wren_call.{cpp,h,md}` — invocar funciones wren desde C++
|
||||
|
||||
## Tamaño objetivo si se hace
|
||||
|
||||
- Wren: ~60 KB total (vm + bindings).
|
||||
- Bindings de juego: ~10 KB.
|
||||
- Maximo aceptable: 100 KB.
|
||||
|
||||
## Criterio para cerrar este issue
|
||||
|
||||
- Decision tomada: si o no.
|
||||
- Si si: VM integrada, bindings minimos, demo de un script de AI cargado en runtime.
|
||||
- Si no: documentar el por qué en `cpp/GAMEDEV.md` (helpful no future contributors).
|
||||
|
||||
## No-objetivos
|
||||
|
||||
- Visual scripting (Blueprints style).
|
||||
- Scripting para todo el juego.
|
||||
- Multiples lenguajes de scripting.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: 0076
|
||||
title: gradle_run no detecta SDK en $HOME/android-sdk (donde lo deja install_android_sdk)
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
related_functions: [gradle_run_bash_infra, install_android_sdk_bash_infra]
|
||||
---
|
||||
|
||||
## Sintoma
|
||||
|
||||
`gradle_run_bash_infra` resuelve `ANDROID_HOME` con default `$HOME/Android/Sdk`. Pero `install_android_sdk_bash_infra` instala en `$HOME/android-sdk` (lowercase, distinto path). Resultado: aunque el usuario tiene SDK Linux instalado via la funcion del propio registry, gradle_run no lo encuentra.
|
||||
|
||||
## Reproducir
|
||||
|
||||
```bash
|
||||
./fn run install_android_sdk
|
||||
ls $HOME/android-sdk/ # OK, contiene cmdline-tools, platforms, etc.
|
||||
|
||||
cd apps/<some_kt_app>
|
||||
./fn run gradle_unit_test .
|
||||
# falla porque ANDROID_HOME apunta a $HOME/Android/Sdk (no existe)
|
||||
```
|
||||
|
||||
## Fix propuesto
|
||||
|
||||
En `bash/functions/infra/gradle_run.sh`, anadir orden de busqueda:
|
||||
|
||||
```bash
|
||||
if [[ -z "${ANDROID_HOME:-}" ]]; then
|
||||
for candidate in \
|
||||
"$HOME/android-sdk" \
|
||||
"$HOME/Android/Sdk" \
|
||||
"${ANDROID_SDK_WIN:-/mnt/c/Users/$USER/AppData/Local/Android/Sdk}"
|
||||
do
|
||||
if [[ -d "$candidate" && -d "$candidate/platform-tools" ]]; then
|
||||
ANDROID_HOME="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
```
|
||||
|
||||
Tambien aplica a `ANDROID_SDK_DIR` para alinear con la funcion install.
|
||||
|
||||
## Validacion
|
||||
|
||||
`unset ANDROID_HOME && bash bash/functions/infra/gradle_run.sh apps/counter_kt :app:tasks` debe corre OK con SDK en `$HOME/android-sdk`.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
id: 0077
|
||||
title: fn run <bash_function> no propaga stdout/stderr al usuario
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
related_components: [cmd/fn, fn run dispatcher]
|
||||
---
|
||||
|
||||
## Sintoma
|
||||
|
||||
```bash
|
||||
./fn run gradle_unit_test apps/counter_kt
|
||||
```
|
||||
|
||||
Salida:
|
||||
```
|
||||
[fn run] gradle_unit_test_bash_infra (bash/function) apps/counter_kt
|
||||
```
|
||||
|
||||
Y nada mas. El subprocess parece correr (gradle se invoca, build sucede), pero stdout y stderr del subproceso no llegan a la terminal del caller.
|
||||
|
||||
## Reproducir
|
||||
|
||||
Comparar:
|
||||
```bash
|
||||
./fn run gradle_unit_test apps/counter_kt # mudo
|
||||
bash bash/functions/infra/gradle_unit_test.sh apps/counter_kt # imprime build log normal
|
||||
```
|
||||
|
||||
## Hipotesis
|
||||
|
||||
`cmd/fn` dispatcher para bash usa `exec.Command(...).Run()` u `Output()` y descarta stdout/stderr en lugar de conectarlos al terminal del caller. O captura todo y lo imprime al final pero solo si exit 0.
|
||||
|
||||
## Fix propuesto
|
||||
|
||||
En el dispatcher de bash (`cmd/fn/run.go` o similar), conectar `cmd.Stdout = os.Stdout` y `cmd.Stderr = os.Stderr` para streaming en tiempo real. Ya se hace para Go/Python segun otros flujos del registry.
|
||||
|
||||
## Validacion
|
||||
|
||||
`./fn run gradle_unit_test apps/counter_kt` debe imprimir mismo output que `bash bash/functions/infra/gradle_unit_test.sh apps/counter_kt`.
|
||||
@@ -91,3 +91,16 @@
|
||||
| [0071b](0071b-extract-jobs-queue-panel.md) | Extraer `jobs_queue_panel` a cpp/functions/core/ (sub-issue de 0071, absorbe 0065) | pendiente | media | refactor | parte de 0071, depende 0071f |
|
||||
| [0071f](0071f-extract-subprocess-streamer.md) | Extraer `subprocess_streamer` a cpp/functions/core/ (sub-issue de 0071) | pendiente | media | refactor | parte de 0071 |
|
||||
| [0071g](0071g-extract-app-db-init.md) | Extraer `app_db_init` a cpp/functions/core/ (sub-issue de 0071, Tier 4) | pendiente | media | refactor | parte de 0071 |
|
||||
| [0072](0072-gamedev-stack-roadmap.md) | gamedev — stack ligero multi-plataforma + crypto (roadmap) | pendiente | media | planning | 0072a-l |
|
||||
| [0072a](0072a-gamedev-smoke-sdl3-sokol-imgui.md) | gamedev — smoke SDL3 + sokol_gfx + ImGui (PC + WASM) | pendiente | alta | feature | parte de 0072 |
|
||||
| [0072b](0072b-gamedev-runtime-core.md) | gamedev — runtime nucleo (sprite batcher, audio, input, game loop) | pendiente | alta | feature | parte de 0072, depende 0072a |
|
||||
| [0072c](0072c-gamedev-asset-pipeline.md) | gamedev — asset pipeline (atlas, MSDF, tilemap, shader translate) | pendiente | alta | feature | parte de 0072, depende 0072b |
|
||||
| [0072d](0072d-gamedev-wasm-build-size-budget.md) | gamedev — WASM build pipeline + size budget | pendiente | alta | feature | parte de 0072, depende 0072a/b |
|
||||
| [0072e](0072e-gamedev-crypto-bridge-web3.md) | gamedev — bridge crypto Web3 (wallets, sign tx) JS interop | pendiente | alta | feature | parte de 0072, depende 0072a/d |
|
||||
| [0072f](0072f-gamedev-crypto-onchain-assets-payments.md) | gamedev — crypto on-chain (NFT assets, payments, leaderboards firmadas) | pendiente | media | feature | parte de 0072, depende 0072e |
|
||||
| [0072g](0072g-gamedev-android-build.md) | gamedev — Android build (NDK + touch + virtual gamepad + WalletConnect) | pendiente | media | feature | parte de 0072, depende 0072b/c |
|
||||
| [0072h](0072h-gamedev-ios-build.md) | gamedev — iOS build (Xcode + Metal via sokol + WalletConnect) | pendiente | media | feature | parte de 0072, depende 0072b/c |
|
||||
| [0072i](0072i-gamedev-editor-app.md) | gamedev — editor visual `game_editor` (scene tree, inspector, asset browser) | pendiente | media | feature | parte de 0072, depende 0072b/c |
|
||||
| [0072j](0072j-gamedev-physics-box2d.md) | gamedev — physics 2D (Box2D integration) | pendiente | media | feature | parte de 0072, depende 0072b |
|
||||
| [0072k](0072k-gamedev-demo-platformer.md) | gamedev — demo plataformero `engine_demo` (referencia stack completo) | pendiente | alta | feature | parte de 0072, depende 0072b/c/d/j |
|
||||
| [0072l](0072l-gamedev-scripting-optional.md) | gamedev — scripting opcional (wren / lua / hot reload) | diferido | baja | feature | parte de 0072 |
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: 0073
|
||||
title: init_kotlin_app genera gradlew stub no funcional
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
related_pipelines: [init_kotlin_app_bash_pipelines]
|
||||
related_apps: [counter_kt]
|
||||
---
|
||||
|
||||
## Sintoma
|
||||
|
||||
Al scaffoldear app Kotlin con `fn run init_kotlin_app <name>`, el `gradlew` generado es un stub que delega a `gradle/wrapper/gradlew` (no existe) y luego a `gradle` system (no instalado). Resultado: silenciosamente falla.
|
||||
|
||||
## Reproducir
|
||||
|
||||
```bash
|
||||
./fn run init_kotlin_app foo_app
|
||||
cd apps/foo_app
|
||||
./gradlew :app:assembleDebug
|
||||
# (no output, exit silente)
|
||||
```
|
||||
|
||||
## Causa
|
||||
|
||||
`bash/functions/pipelines/init_kotlin_app.sh` linea ~XXX genera placeholder gradlew. No descarga `gradle-wrapper.jar` real ni invoca `gradle wrapper --gradle-version 8.6`.
|
||||
|
||||
## Workaround actual
|
||||
|
||||
Copiar wrapper de otra app:
|
||||
```bash
|
||||
cp apps/voice_guide/frontend/android/gradle/wrapper/gradle-wrapper.jar <new_app>/gradle/wrapper/
|
||||
cp apps/voice_guide/frontend/android/gradlew <new_app>/gradlew
|
||||
chmod +x <new_app>/gradlew
|
||||
```
|
||||
|
||||
## Fix propuesto
|
||||
|
||||
Una de:
|
||||
- Descargar `gradle-wrapper.jar` desde `https://raw.githubusercontent.com/gradle/gradle/v8.6.0/gradle/wrapper/gradle-wrapper.jar` (~60KB) durante scaffold + escribir `gradlew` real (no stub).
|
||||
- Vendor del wrapper jar en `bash/functions/pipelines/templates/kotlin/gradle-wrapper.jar` y copiar al scaffold.
|
||||
- Detectar gradle system, invocar `gradle wrapper --gradle-version 8.6` post-mkdir.
|
||||
|
||||
Recomendado: vendor (option B) — sin dependencia de red en cada scaffold.
|
||||
|
||||
## Validacion
|
||||
|
||||
Tras fix, `./fn run init_kotlin_app smoke && cd apps/smoke && ./gradlew --version` debe imprimir Gradle 8.6 sin errores.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: 0074
|
||||
title: init_kotlin_app no genera local.properties
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
related_pipelines: [init_kotlin_app_bash_pipelines]
|
||||
related_apps: [counter_kt]
|
||||
---
|
||||
|
||||
## Sintoma
|
||||
|
||||
Tras scaffoldear, gradle aborta con:
|
||||
```
|
||||
SDK location not found. Define a valid SDK location with an ANDROID_HOME environment variable or by setting the sdk.dir path in your project's local properties file at '<dir>/local.properties'.
|
||||
```
|
||||
|
||||
## Causa
|
||||
|
||||
Scaffolder no genera `local.properties`. Aunque `gradle_run_bash_infra` exporta ANDROID_HOME, IDEs (Android Studio) y devs que usen `gradle` directamente no heredan ese env.
|
||||
|
||||
## Fix propuesto
|
||||
|
||||
En `init_kotlin_app.sh` tras crear estructura, generar:
|
||||
|
||||
```bash
|
||||
cat > "$abs_dir/local.properties" <<EOF
|
||||
# Auto-generated by init_kotlin_app. Per-machine, gitignore'd.
|
||||
sdk.dir=${ANDROID_SDK_DIR:-$HOME/android-sdk}
|
||||
EOF
|
||||
```
|
||||
|
||||
Y anadir `local.properties` al `.gitignore` del scaffold.
|
||||
|
||||
## Validacion
|
||||
|
||||
`fn run init_kotlin_app smoke && cd apps/smoke && ./gradlew tasks` debe ejecutar sin error de SDK location.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
id: 0075
|
||||
title: init_kotlin_app genera AndroidManifest con tema AppCompat sin dep
|
||||
status: pending
|
||||
priority: medium
|
||||
created: 2026-05-10
|
||||
related_pipelines: [init_kotlin_app_bash_pipelines]
|
||||
related_apps: [counter_kt]
|
||||
fix_applied: 2026-05-10
|
||||
---
|
||||
|
||||
## Sintoma
|
||||
|
||||
```
|
||||
ERROR: AAPT: error: resource style/Theme.AppCompat.Light.NoActionBar not found
|
||||
```
|
||||
|
||||
## Causa
|
||||
|
||||
`init_kotlin_app.sh` template AndroidManifest.xml usaba `@style/Theme.AppCompat.Light.NoActionBar` pero `app/build.gradle.kts` no incluye `androidx.appcompat:appcompat`. Apps Compose no necesitan AppCompat — usan tema Android plataforma.
|
||||
|
||||
## Fix aplicado (2026-05-10)
|
||||
|
||||
`init_kotlin_app.sh` linea 240: cambiado a `@android:style/Theme.Material.Light.NoActionBar` (built-in, sin dep externa).
|
||||
|
||||
## Validacion
|
||||
|
||||
Smoke test counter_kt build: ✓ 8.3MB APK generado tras fix.
|
||||
|
||||
## Cierre
|
||||
|
||||
Mover este archivo a `dev/issues/completed/` cuando se confirme en proxima app scaffolded.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: 0078
|
||||
title: kotlin/functions/ui requiere ser modulo Gradle library para composite build
|
||||
status: pending
|
||||
priority: high
|
||||
created: 2026-05-10
|
||||
fix_applied: 2026-05-10
|
||||
related_functions:
|
||||
- fn_colors_kt_ui
|
||||
- fn_spacing_kt_ui
|
||||
- fn_radius_kt_ui
|
||||
- fn_typography_kt_ui
|
||||
- fn_shadows_kt_ui
|
||||
- fn_tokens_kt_ui
|
||||
- fn_theme_kt_ui
|
||||
related_apps: [counter_kt]
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
Apps Kotlin Compose nuevas (counter_kt y futuras) consumen `kotlin/functions/ui/` via Gradle composite build (`includeBuild` en settings.gradle.kts). Para que funcione, `kotlin/functions/ui/` debe ser un proyecto Gradle valido (com.android.library + Compose).
|
||||
|
||||
Inicialmente solo habia `.kt` + `.md` sueltos sin estructura Gradle.
|
||||
|
||||
## Fix aplicado (2026-05-10)
|
||||
|
||||
Anadidos a `kotlin/functions/ui/`:
|
||||
- `settings.gradle.kts` (rootProject.name = "fn-compose-ui")
|
||||
- `build.gradle.kts` (com.android.library + kotlin-android + Compose 2024.02.00)
|
||||
- `gradle.properties` (jvmargs, useAndroidX)
|
||||
- `src/main/AndroidManifest.xml` (vacio, package via namespace)
|
||||
|
||||
Movidos: 7 `.kt` desde `kotlin/functions/ui/*.kt` → `kotlin/functions/ui/src/main/kotlin/fn/compose/theme/*.kt`. Estructura Gradle estandar.
|
||||
|
||||
Actualizado `file_path` en 7 `.md` apuntando al nuevo path. `.md` se quedan en `kotlin/functions/ui/` (raiz del modulo) — registry los detecta normal.
|
||||
|
||||
## Pendientes
|
||||
|
||||
1. **Anadir wrapper Gradle propio al modulo** — actualmente depende de que el caller (counter_kt) bootstrap gradle. Sin wrapper propio, no se puede `cd kotlin/functions/ui && ./gradlew tasks` para inspeccionar.
|
||||
2. **Tests propios del modulo** — anadir `src/test/kotlin/.../FnColorsTest.kt` etc. para validar tokens (ej. assertEquals hex codes Mantine spec).
|
||||
3. **`.gitignore` propio** — `build/`, `.gradle/`, `local.properties`.
|
||||
4. **Documentacion `kotlin/functions/ui/README.md`** — como anadir un nuevo componente, convencion package, layout.
|
||||
|
||||
## Validacion
|
||||
|
||||
`fn run init_kotlin_app smoke && cd apps/smoke && ./gradlew :app:assembleDebug` corre y enlaza correctamente con `fn.compose:ui` del composite build (counter_kt confirmado funcionando 2026-05-10).
|
||||
Reference in New Issue
Block a user