backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

View File

@@ -6,10 +6,10 @@ doubt, link back to the relevant guide instead of duplicating long explanations.
## 1. Launching Common Editors
```bash
# Open YAZE directly in the Dungeon editor with room cards preset
# Open YAZE directly in the Dungeon editor with room panels preset
./build/bin/yaze --rom_file=zelda3.sfc \
--editor=Dungeon \
--cards="Rooms List,Room Graphics,Object Editor"
--open_panels="Rooms List,Room Graphics,Object Editor"
# Jump to an Overworld map from the CLI/TUI companion
./build/bin/z3ed overworld describe-map --map 0x80 --rom zelda3.sfc
@@ -34,9 +34,9 @@ cmake --preset mac-dbg
cmake --build --preset mac-dbg --target yaze yaze_test
./build/bin/yaze_test --unit
# AI-focused build in a dedicated directory (recommended for assistants)
cmake --preset mac-ai -B build_ai
cmake --build build_ai --target yaze z3ed
# AI-focused build
cmake --preset mac-ai
cmake --build --preset mac-ai --target yaze z3ed
```
## 4. Quick Verification

View File

@@ -0,0 +1,182 @@
// Example: How to use WasmPatchExport in the yaze editor
// This code would typically be integrated into the ROM file manager or editor menu
#include "app/platform/wasm/wasm_patch_export.h"
#include "app/rom.h"
#include "imgui.h"
namespace yaze {
namespace editor {
// Example function that could be added to RomFileManager or MenuOrchestrator
void ShowPatchExportDialog(Rom* rom) {
static bool show_export_dialog = false;
static int patch_format = 0; // 0 = BPS, 1 = IPS
static char filename[256] = "my_hack";
// Menu item to trigger export
if (ImGui::MenuItem("Export Patch...", nullptr, nullptr, rom->is_loaded())) {
show_export_dialog = true;
}
// Export dialog window
if (show_export_dialog) {
ImGui::OpenPopup("Export Patch");
}
if (ImGui::BeginPopupModal("Export Patch", &show_export_dialog)) {
// Get the original ROM data (assuming rom stores both original and modified)
const auto& original_data = rom->original_data(); // Would need to add this
const auto& modified_data = rom->data();
// Show patch preview information
auto patch_info = platform::WasmPatchExport::GetPatchPreview(
original_data, modified_data);
ImGui::Text("Patch Summary:");
ImGui::Separator();
ImGui::Text("Total changed bytes: %zu", patch_info.changed_bytes);
ImGui::Text("Number of regions: %zu", patch_info.num_regions);
if (patch_info.changed_bytes == 0) {
ImGui::TextColored(ImVec4(1, 1, 0, 1), "No changes detected!");
}
// Show changed regions (limit to first 10 for UI)
if (!patch_info.changed_regions.empty()) {
ImGui::Separator();
ImGui::Text("Changed Regions:");
int region_count = 0;
for (const auto& region : patch_info.changed_regions) {
if (region_count >= 10) {
ImGui::Text("... and %zu more regions",
patch_info.changed_regions.size() - 10);
break;
}
ImGui::Text(" Offset: 0x%06X, Size: %zu bytes",
static_cast<unsigned>(region.first), region.second);
region_count++;
}
}
ImGui::Separator();
// Format selection
ImGui::Text("Patch Format:");
ImGui::RadioButton("BPS (Beat)", &patch_format, 0);
ImGui::SameLine();
ImGui::RadioButton("IPS", &patch_format, 1);
// Filename input
ImGui::Text("Filename:");
ImGui::InputText("##filename", filename, sizeof(filename));
// Export buttons
ImGui::Separator();
if (ImGui::Button("Export", ImVec2(120, 0))) {
if (patch_info.changed_bytes > 0) {
absl::Status status;
std::string full_filename = std::string(filename);
if (patch_format == 0) {
// BPS format
if (full_filename.find(".bps") == std::string::npos) {
full_filename += ".bps";
}
status = platform::WasmPatchExport::ExportBPS(
original_data, modified_data, full_filename);
} else {
// IPS format
if (full_filename.find(".ips") == std::string::npos) {
full_filename += ".ips";
}
status = platform::WasmPatchExport::ExportIPS(
original_data, modified_data, full_filename);
}
if (status.ok()) {
ImGui::CloseCurrentPopup();
show_export_dialog = false;
// Could show success toast here
} else {
// Show error message
ImGui::OpenPopup("Export Error");
}
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
show_export_dialog = false;
}
// Error popup
if (ImGui::BeginPopupModal("Export Error")) {
ImGui::Text("Failed to export patch!");
if (ImGui::Button("OK", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::EndPopup();
}
}
// Alternative: Quick export functions for toolbar/menu
void QuickExportBPS(Rom* rom) {
#ifdef __EMSCRIPTEN__
if (!rom || !rom->is_loaded()) return;
const auto& original = rom->original_data(); // Would need to add this
const auto& modified = rom->data();
// Generate default filename based on ROM name
std::string filename = rom->filename();
size_t dot_pos = filename.find_last_of('.');
if (dot_pos != std::string::npos) {
filename = filename.substr(0, dot_pos);
}
filename += ".bps";
auto status = platform::WasmPatchExport::ExportBPS(
original, modified, filename);
if (!status.ok()) {
// Show error toast or log
}
#endif
}
void QuickExportIPS(Rom* rom) {
#ifdef __EMSCRIPTEN__
if (!rom || !rom->is_loaded()) return;
const auto& original = rom->original_data();
const auto& modified = rom->data();
// Check IPS size limit
if (modified.size() > 0xFFFFFF) {
// Show error: ROM too large for IPS format
return;
}
std::string filename = rom->filename();
size_t dot_pos = filename.find_last_of('.');
if (dot_pos != std::string::npos) {
filename = filename.substr(0, dot_pos);
}
filename += ".ips";
auto status = platform::WasmPatchExport::ExportIPS(
original, modified, filename);
if (!status.ok()) {
// Show error toast or log
}
#endif
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,241 @@
// Example: Using WasmMessageQueue with WasmCollaboration
// This example shows how the collaboration system can use the message queue
// for offline support and automatic replay when reconnecting.
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_collaboration.h"
#include "app/platform/wasm/wasm_message_queue.h"
#include <emscripten.h>
#include <memory>
namespace yaze {
namespace app {
namespace platform {
// Example integration class that combines collaboration with offline queue
class CollaborationWithOfflineSupport {
public:
CollaborationWithOfflineSupport()
: collaboration_(std::make_unique<WasmCollaboration>()),
message_queue_(std::make_unique<WasmMessageQueue>()) {
// Configure message queue
message_queue_->SetAutoPersist(true);
message_queue_->SetMaxQueueSize(500);
message_queue_->SetMessageExpiry(86400.0); // 24 hours
// Set up callbacks
SetupCallbacks();
// Load any previously queued messages
auto status = message_queue_->LoadFromStorage();
if (!status.ok()) {
emscripten_log(EM_LOG_WARN, "Failed to load offline queue: %s",
status.ToString().c_str());
}
}
// Send a change, queuing if offline
void SendChange(uint32_t offset, const std::vector<uint8_t>& old_data,
const std::vector<uint8_t>& new_data) {
if (collaboration_->IsConnected()) {
// Try to send directly
auto status = collaboration_->BroadcastChange(offset, old_data, new_data);
if (!status.ok()) {
// Failed to send, queue for later
QueueChange(offset, old_data, new_data);
}
} else {
// Not connected, queue for later
QueueChange(offset, old_data, new_data);
}
}
// Send cursor position, queuing if offline
void SendCursorPosition(const std::string& editor_type, int x, int y, int map_id) {
if (collaboration_->IsConnected()) {
auto status = collaboration_->SendCursorPosition(editor_type, x, y, map_id);
if (!status.ok()) {
QueueCursorPosition(editor_type, x, y, map_id);
}
} else {
QueueCursorPosition(editor_type, x, y, map_id);
}
}
// Called when connection is established
void OnConnectionEstablished() {
emscripten_log(EM_LOG_INFO, "Connection established, replaying queued messages...");
// Create sender function that uses the collaboration instance
auto sender = [this](const std::string& message_type, const std::string& payload) -> absl::Status {
// Parse the payload and send via collaboration
try {
nlohmann::json data = nlohmann::json::parse(payload);
if (message_type == "change") {
uint32_t offset = data["offset"];
std::vector<uint8_t> old_data = data["old_data"];
std::vector<uint8_t> new_data = data["new_data"];
return collaboration_->BroadcastChange(offset, old_data, new_data);
} else if (message_type == "cursor") {
std::string editor_type = data["editor_type"];
int x = data["x"];
int y = data["y"];
int map_id = data.value("map_id", -1);
return collaboration_->SendCursorPosition(editor_type, x, y, map_id);
}
return absl::InvalidArgumentError("Unknown message type: " + message_type);
} catch (const std::exception& e) {
return absl::InvalidArgumentError("Failed to parse payload: " + std::string(e.what()));
}
};
// Replay all queued messages
message_queue_->ReplayAll(sender, 3); // Max 3 retries per message
}
// Get queue status for UI display
WasmMessageQueue::QueueStatus GetQueueStatus() const {
return message_queue_->GetStatus();
}
// Clear all queued messages
void ClearQueue() {
message_queue_->Clear();
}
// Prune old messages
void PruneOldMessages() {
int removed = message_queue_->PruneExpiredMessages();
if (removed > 0) {
emscripten_log(EM_LOG_INFO, "Pruned %d expired messages", removed);
}
}
private:
void SetupCallbacks() {
// Set up replay complete callback
message_queue_->SetOnReplayComplete([](int replayed, int failed) {
emscripten_log(EM_LOG_INFO, "Replay complete: %d sent, %d failed", replayed, failed);
// Show notification to user
EM_ASM({
if (window.showNotification) {
const message = `Synced ${$0} changes` + ($1 > 0 ? `, ${$1} failed` : '');
window.showNotification(message, $1 > 0 ? 'warning' : 'success');
}
}, replayed, failed);
});
// Set up status change callback
message_queue_->SetOnStatusChange([](const WasmMessageQueue::QueueStatus& status) {
// Update UI with queue status
EM_ASM({
if (window.updateQueueStatus) {
window.updateQueueStatus({
pendingCount: $0,
failedCount: $1,
totalBytes: $2,
oldestMessageAge: $3,
isPersisted: $4
});
}
}, status.pending_count, status.failed_count, status.total_bytes,
status.oldest_message_age, status.is_persisted);
});
// Set up collaboration status callback
collaboration_->SetStatusCallback([this](bool connected, const std::string& message) {
if (connected) {
// Connection established, replay queued messages
OnConnectionEstablished();
} else {
// Connection lost
emscripten_log(EM_LOG_INFO, "Connection lost: %s", message.c_str());
}
});
}
void QueueChange(uint32_t offset, const std::vector<uint8_t>& old_data,
const std::vector<uint8_t>& new_data) {
nlohmann::json payload;
payload["offset"] = offset;
payload["old_data"] = old_data;
payload["new_data"] = new_data;
payload["timestamp"] = emscripten_get_now() / 1000.0;
std::string msg_id = message_queue_->Enqueue("change", payload.dump());
emscripten_log(EM_LOG_DEBUG, "Queued change message: %s", msg_id.c_str());
}
void QueueCursorPosition(const std::string& editor_type, int x, int y, int map_id) {
nlohmann::json payload;
payload["editor_type"] = editor_type;
payload["x"] = x;
payload["y"] = y;
if (map_id >= 0) {
payload["map_id"] = map_id;
}
payload["timestamp"] = emscripten_get_now() / 1000.0;
std::string msg_id = message_queue_->Enqueue("cursor", payload.dump());
emscripten_log(EM_LOG_DEBUG, "Queued cursor message: %s", msg_id.c_str());
}
std::unique_ptr<WasmCollaboration> collaboration_;
std::unique_ptr<WasmMessageQueue> message_queue_;
};
// JavaScript bindings for the enhanced collaboration
extern "C" {
// Create collaboration instance with offline support
EMSCRIPTEN_KEEPALIVE
void* create_collaboration_with_offline() {
return new CollaborationWithOfflineSupport();
}
// Send a change (with automatic queuing if offline)
EMSCRIPTEN_KEEPALIVE
void send_change_with_queue(void* instance, uint32_t offset,
uint8_t* old_data, int old_size,
uint8_t* new_data, int new_size) {
auto* collab = static_cast<CollaborationWithOfflineSupport*>(instance);
std::vector<uint8_t> old_vec(old_data, old_data + old_size);
std::vector<uint8_t> new_vec(new_data, new_data + new_size);
collab->SendChange(offset, old_vec, new_vec);
}
// Get queue status
EMSCRIPTEN_KEEPALIVE
int get_pending_message_count(void* instance) {
auto* collab = static_cast<CollaborationWithOfflineSupport*>(instance);
return collab->GetQueueStatus().pending_count;
}
// Clear offline queue
EMSCRIPTEN_KEEPALIVE
void clear_offline_queue(void* instance) {
auto* collab = static_cast<CollaborationWithOfflineSupport*>(instance);
collab->ClearQueue();
}
// Prune old messages
EMSCRIPTEN_KEEPALIVE
void prune_old_messages(void* instance) {
auto* collab = static_cast<CollaborationWithOfflineSupport*>(instance);
collab->PruneOldMessages();
}
} // extern "C"
} // namespace platform
} // namespace app
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,255 @@
# Web Terminal Integration for Z3ed
This document describes how to integrate the z3ed terminal functionality into the WASM web build.
## Overview
The `wasm_terminal_bridge.cc` file provides C++ functions that can be called from JavaScript to enable z3ed command processing in the browser. This allows users to interact with ROM data and use AI-powered features directly in the web interface.
## Exported Functions
### Core Functions
```javascript
// Process a z3ed command
const char* Z3edProcessCommand(const char* command);
// Get command completions for autocomplete
const char* Z3edGetCompletions(const char* partial);
// Set API key for AI services (Gemini)
void Z3edSetApiKey(const char* api_key);
// Check if terminal bridge is ready
int Z3edIsReady();
// Load ROM data from ArrayBuffer
int Z3edLoadRomData(const uint8_t* data, size_t size);
// Get current ROM information as JSON
const char* Z3edGetRomInfo();
// Execute resource queries
const char* Z3edQueryResource(const char* query);
```
## JavaScript Integration Example
```javascript
// Initialize the terminal when module is ready
Module.onRuntimeInitialized = function() {
// Check if terminal is ready
if (Module.ccall('Z3edIsReady', 'number', [], [])) {
console.log('Z3ed terminal ready');
}
// Set API key for AI features
const apiKey = localStorage.getItem('gemini_api_key');
if (apiKey) {
Module.ccall('Z3edSetApiKey', null, ['string'], [apiKey]);
}
// Create terminal interface
const terminal = new Terminal({
prompt: 'z3ed> ',
onCommand: (cmd) => {
const result = Module.ccall('Z3edProcessCommand', 'string', ['string'], [cmd]);
terminal.print(result);
},
onTab: (partial) => {
const completions = Module.ccall('Z3edGetCompletions', 'string', ['string'], [partial]);
return JSON.parse(completions);
}
});
// Expose terminal globally
window.z3edTerminal = terminal;
};
// Load ROM file
async function loadRomFile(file) {
const arrayBuffer = await file.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
// Allocate memory in WASM heap
const ptr = Module._malloc(data.length);
Module.HEAPU8.set(data, ptr);
// Load ROM
const success = Module.ccall('Z3edLoadRomData', 'number',
['number', 'number'], [ptr, data.length]);
// Free memory
Module._free(ptr);
if (success) {
// Get ROM info
const info = Module.ccall('Z3edGetRomInfo', 'string', [], []);
console.log('ROM loaded:', JSON.parse(info));
}
}
```
## Terminal UI Component
```javascript
class Z3edTerminal {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.history = [];
this.historyIndex = 0;
this.setupUI();
}
setupUI() {
this.container.innerHTML = `
<div class="terminal-output"></div>
<div class="terminal-input">
<span class="prompt">z3ed> </span>
<input type="text" class="command-input" />
</div>
`;
this.output = this.container.querySelector('.terminal-output');
this.input = this.container.querySelector('.command-input');
this.input.addEventListener('keydown', (e) => this.handleKeydown(e));
}
handleKeydown(e) {
if (e.key === 'Enter') {
this.executeCommand(this.input.value);
this.history.push(this.input.value);
this.historyIndex = this.history.length;
this.input.value = '';
} else if (e.key === 'Tab') {
e.preventDefault();
this.handleAutocomplete();
} else if (e.key === 'ArrowUp') {
this.navigateHistory(-1);
} else if (e.key === 'ArrowDown') {
this.navigateHistory(1);
}
}
executeCommand(cmd) {
this.print(`z3ed> ${cmd}`, 'command');
if (!Module.ccall) {
this.printError('WASM module not loaded');
return;
}
try {
const result = Module.ccall('Z3edProcessCommand', 'string', ['string'], [cmd]);
this.print(result);
} catch (error) {
this.printError(`Error: ${error.message}`);
}
}
handleAutocomplete() {
const partial = this.input.value;
const completions = Module.ccall('Z3edGetCompletions', 'string', ['string'], [partial]);
const options = JSON.parse(completions);
if (options.length === 1) {
this.input.value = options[0];
} else if (options.length > 1) {
this.print(`Available commands: ${options.join(', ')}`);
}
}
navigateHistory(direction) {
this.historyIndex = Math.max(0, Math.min(this.history.length, this.historyIndex + direction));
this.input.value = this.history[this.historyIndex] || '';
}
print(text, className = 'output') {
const line = document.createElement('div');
line.className = className;
line.textContent = text;
this.output.appendChild(line);
this.output.scrollTop = this.output.scrollHeight;
}
printError(text) {
this.print(text, 'error');
}
clear() {
this.output.innerHTML = '';
}
}
```
## Available Commands
The WASM build includes a subset of z3ed commands that don't require native dependencies:
### Basic Commands
- `help` - Show available commands
- `help <category>` - Show commands in a category
- `clear` - Clear terminal output
- `version` - Show version information
### ROM Commands
- `rom load <file>` - Load ROM from file
- `rom info` - Display ROM information
- `rom validate` - Validate ROM structure
### Resource Queries
- `resource query dungeon.rooms` - List dungeon rooms
- `resource query overworld.maps` - List overworld maps
- `resource query graphics.sheets` - List graphics sheets
- `resource query palettes` - List palettes
### AI Commands (requires API key)
- `ai <prompt>` - Generate AI response
- `ai analyze <resource>` - Analyze ROM resource
- `ai suggest <context>` - Get suggestions
### Graphics Commands
- `gfx list` - List graphics resources
- `gfx export <id>` - Export graphics (returns base64)
## Build Configuration
The WASM terminal bridge is automatically included when building with Emscripten:
```bash
# Configure for WASM with AI support
cmake --preset wasm-ai
# Build
cmake --build build --target yaze
# The resulting files will be:
# - yaze.js (JavaScript loader)
# - yaze.wasm (WebAssembly module)
# - yaze.html (Example HTML page)
```
## Security Considerations
1. **API Keys**: Store API keys in sessionStorage or localStorage, never hardcode them
2. **ROM Data**: ROM data stays in browser memory, never sent to servers
3. **CORS**: AI API requests go through browser fetch, respecting CORS policies
4. **Sandboxing**: WASM runs in browser sandbox with limited filesystem access
## Troubleshooting
### Module not loading
- Ensure WASM files are served with correct MIME type: `application/wasm`
- Check browser console for CORS errors
- Verify SharedArrayBuffer support if using threads
### Commands not working
- Check if ROM is loaded: `Z3edGetRomInfo()`
- Verify terminal is ready: `Z3edIsReady()`
- Check browser console for error messages
### AI features not working
- Ensure API key is set: `Z3edSetApiKey()`
- Check network tab for API request failures
- Verify Gemini API quota and limits