Stable state

This commit is contained in:
Andreas Schaafsma 2025-04-10 03:19:07 +02:00
parent 8c36316409
commit 27c5503173
13 changed files with 1351 additions and 237 deletions

View File

@ -0,0 +1,5 @@
# Never violate the following rules:
- Make the code work with the .hxml based workflow
- don't use compiletime `#if lime` or `#if openfl` as this relies on the openfl and lime build system. We don't use their buildsystems but try to do the same thing they do for the hxcpp based targets. (Native Windows/ Linux compilation)
- Adhere to the spec outlined in OpenFL_Build_Chain_Asset_System_Deep_dive.md and IMPORTANT_INSTRUCTIONS.md

View File

@ -0,0 +1,22 @@
## Important Development Instructions
In this repository you will find 3 projects.
One of them is in the root and src directories of this repo, this is the project we're actively working on.
The other 2 are located at
- ./lime-repo
- ./openfl-repo
Instructions:
Analyze the openfl repo, this project contains a build chain based on HXP that can be called from the commandline. We need to document how this buildchain operates when the following command is ran.
`openfl test linux`
We already know that it makes some calls to lime under the hood which has a similar build chain.
The current project is built to create an openfl app using the hxml build system and bypass lime's/ openfl's build system entirely.
We are currently working on asset bundling, this is proving to be a difficult task. Please document how openfl does this in a file called BUILD_CHAIN_DOCUMENTATION_OPENFL.md

View File

@ -0,0 +1,558 @@
# OpenFL Build Chain: Asset System Deep Dive
This document provides an in-depth analysis of OpenFL's asset handling system, focusing on asset processing, the runtime access layer, and resource management. This information is intended to help implement a custom asset bundling system that bypasses the standard OpenFL/Lime build toolchain.
## 1. Asset Processing System
OpenFL's asset processing pipeline transforms source files into platform-optimized assets through several stages:
### 1.1 Asset Identification
**Implementation:**
- The asset processing begins with scanning directories specified in `project.xml`
- For each asset, OpenFL records:
- **Source path**: Location in the project directory
- **Target path**: Location in the output bundle
- **ID**: Unique identifier (typically the target path)
- **Type**: Determined by file extension or explicit definition
```haxe
// Simplified representation of how OpenFL identifies assets
var assets = [];
for (assetPath in FileSystem.readDirectory(sourcePath)) {
if (filterPattern.match(assetPath)) {
var id = targetPath + assetPath.substr(sourcePath.length);
var type = determineAssetType(assetPath);
assets.push({ id: id, path: assetPath, type: type });
}
}
```
### 1.2 Type-Specific Processing
OpenFL processes different asset types with specialized handlers:
#### Images (BitmapData)
- **Formats**: PNG, JPG, GIF, BMP, etc.
- **Processing**:
- Conversion to platform-specific formats
- Optional compression
- Resolution handling for multi-DPI targets
- For embedded assets, images are encoded as Base64 or binary data within generated code
```haxe
// Example of image processing logic
function processImage(path, targetPath, options) {
var image = Image.fromFile(path);
// Apply transformations based on options
if (options.compress) image.compress(options.quality);
if (options.resize) image.resize(options.width, options.height);
// Output in appropriate format
if (options.embed) {
return encodeImageForEmbedding(image, options.format);
} else {
image.save(targetPath, options.format);
return { path: targetPath };
}
}
```
#### Audio
- **Formats**: MP3, OGG, WAV
- **Processing**:
- Format conversion based on platform support
- Optional compression
- Stream vs. complete load settings
#### Fonts
- **Processing**:
- Registration with the text engine
- Subsetting when specified
- Conversion to platform-specific formats
#### Text/Data Files
- **Processing**:
- Optional minification for JSON/XML
- Encoding conversion if necessary
- Direct embedding or file copying
### 1.3 Asset Transformation Chain
For complex asset types, OpenFL applies a chain of transformations:
1. **Loading**: Reading the source asset
2. **Validation**: Checking format compatibility
3. **Transformation**: Applying format-specific conversions
4. **Optimization**: Platform-specific optimizations
5. **Output**: Writing to target location or embedding
### 1.4 Embedding Process
For embedded assets, OpenFL:
1. Reads the asset content
2. Converts it to a code representation (usually a byte array)
3. Generates code to reconstruct the asset at runtime
4. Includes this code in the compiled application
```haxe
// Example of how embedding is implemented
var content = File.getBytes(assetPath);
var assetCode = 'var ${safeName}_bytes = new haxe.io.Bytes(${content.length}, "${Base64.encode(content)}");';
assetCode += 'assets.byteData.set("${assetId}", ${safeName}_bytes);';
```
### 1.5 Manifest Generation
The final step creates a manifest that catalogs all assets:
```json
{
"name": "default",
"assets": [
{
"id": "assets/images/logo.png",
"path": "./assets/images/logo.png",
"type": "image"
},
{
"id": "assets/data/levels.json",
"path": "./assets/data/levels.json",
"type": "text"
}
]
}
```
## 2. Runtime Access Layer
The runtime access layer is responsible for loading assets during application execution.
### 2.1 Assets Class Architecture
OpenFL's asset system is built around several key classes:
- **Assets** (`openfl.utils.Assets`): Static interface for accessing assets
- **AssetLibrary** (`openfl.utils.AssetLibrary`): Container for a collection of assets
- **AssetCache** (`openfl.utils.AssetCache`): Temporary storage for loaded assets
```haxe
// Core Assets class simplified structure
class Assets {
private static var libraries:Map<String, AssetLibrary>;
private static var cache:AssetCache;
public static function getBitmapData(id:String, useCache:Bool = true):BitmapData;
public static function getBytes(id:String):Bytes;
public static function getFont(id:String):Font;
public static function getSound(id:String):Sound;
public static function getText(id:String):String;
}
```
### 2.2 Asset Resolution System
When requesting an asset:
1. The Assets class parses the ID to determine the library and asset path
2. It checks if the asset is already cached (if caching is enabled)
3. If not cached, it delegates loading to the appropriate AssetLibrary
4. The loaded asset is optionally cached before being returned
```haxe
// Asset resolution pseudocode
function getAsset(id:String, type:AssetType) {
var libraryName = parseLibraryName(id);
var assetPath = parseAssetPath(id);
var library = libraries.get(libraryName);
if (useCache && cache.has(id)) {
return cache.get(id);
}
var asset = library.loadAsset(assetPath, type);
if (useCache) {
cache.set(id, asset);
}
return asset;
}
```
### 2.3 Library Loading Process
Asset libraries are typically loaded at application startup:
```haxe
// How OpenFL initializes the asset system
public static function initializeAssets():Void {
// Load embedded manifest(s)
var manifest = AssetManifest.fromBytes(getEmbeddedManifest());
var library = AssetLibrary.fromManifest(manifest);
// Register the library
registerLibrary("default", library);
// Additional libraries can be loaded dynamically
}
```
### 2.4 Asset Loading Methods
OpenFL supports both synchronous and asynchronous loading:
- **Synchronous**: `Assets.getBitmapData(id)` - Blocks until the asset is loaded
- **Asynchronous**: `Assets.loadBitmapData(id).onComplete(callback)` - Non-blocking load
```haxe
// Example of async loading implementation
public static function loadBitmapData(id:String):Future<BitmapData> {
var libraryName = parseLibraryName(id);
var assetPath = parseAssetPath(id);
var library = libraries.get(libraryName);
return library.loadBitmapData(assetPath);
}
```
## 3. Resource Management
Resource management focuses on efficiently handling assets during runtime.
### 3.1 Asset Caching System
OpenFL's caching system:
1. Stores loaded assets by ID in type-specific caches
2. Enables efficient reuse of frequently accessed assets
3. Provides cache control methods to manage memory usage
```haxe
// AssetCache implementation outline
class AssetCache {
private var bitmapData:Map<String, BitmapData>;
private var font:Map<String, Font>;
private var sound:Map<String, Sound>;
public function set(id:String, asset:Dynamic, type:AssetType):Void {
switch (type) {
case AssetType.IMAGE:
bitmapData.set(id, asset);
case AssetType.FONT:
font.set(id, asset);
// Other types...
}
}
public function get(id:String, type:AssetType):Dynamic {
switch (type) {
case AssetType.IMAGE:
return bitmapData.get(id);
// Other types...
}
return null;
}
public function clear(type:AssetType = null):Void {
// Clear specific or all caches
}
}
```
### 3.2 Memory Management
OpenFL implements several strategies for efficient memory usage:
#### Reference Counting
- Certain assets implement reference counting to track usage
- When references drop to zero, assets can be unloaded
```haxe
class ManagedAsset {
private var referenceCount:Int = 0;
public function retain():Void {
referenceCount++;
}
public function release():Void {
referenceCount--;
if (referenceCount <= 0) {
dispose();
}
}
private function dispose():Void {
// Resource-specific cleanup
}
}
```
#### Cache Eviction Policies
- Time-based: Assets unused for a period are removed
- Size-based: Cache enforces a maximum memory footprint
- Priority-based: Critical assets remain cached longer
#### Manual Control
- `Assets.cache.clear()` - Clears all cached assets
- `Assets.cache.removeBitmapData(id)` - Removes specific cached assets
- `Assets.unloadLibrary(name)` - Unloads entire libraries
### 3.3 Event System
The asset system uses events to communicate state changes:
```haxe
// Event system for asset loading
class AssetLibrary {
private var eventDispatcher:EventDispatcher;
public function loadBitmapData(id:String):Future<BitmapData> {
var future = new Future<BitmapData>();
// Start loading process
var loader = new BitmapDataLoader(id);
loader.onComplete = function(bitmapData) {
eventDispatcher.dispatchEvent(new AssetEvent(AssetEvent.ASSET_LOADED, id));
future.complete(bitmapData);
};
loader.onError = function(error) {
eventDispatcher.dispatchEvent(new AssetEvent(AssetEvent.ASSET_ERROR, id));
future.error(error);
};
loader.load();
return future;
}
}
```
## 4. Implementation Guidelines for Custom Asset System
Based on OpenFL's approach, here are guidelines for implementing your own asset bundling system:
### 4.1 Asset Processing Implementation
1. **Asset Scanner**:
- Create a recursive directory scanner that identifies assets
- Implement pattern matching for inclusion/exclusion rules
- Generate unique IDs based on file paths
```haxe
function scanAssets(directory:String, patterns:Array<String>):Array<AssetInfo> {
var assets = [];
for (file in FileSystem.readRecursive(directory)) {
if (matchesAnyPattern(file, patterns)) {
assets.push({
id: generateAssetId(file, directory),
path: file,
type: determineType(file)
});
}
}
return assets;
}
```
2. **Asset Processors**:
- Create processor classes for each asset type
- Implement transformation pipelines
- Support embedding and non-embedding modes
```haxe
interface AssetProcessor {
function process(source:String, target:String, options:Map<String, Dynamic>):ProcessedAsset;
function embed(source:String, options:Map<String, Dynamic>):EmbeddedCode;
}
class ImageProcessor implements AssetProcessor {
public function process(source, target, options) {
// Image-specific processing
}
public function embed(source, options) {
// Generate code for embedded image
}
}
```
3. **Manifest Generator**:
- Create a serializable manifest structure
- Support JSON and binary formats
- Include asset metadata for runtime access
```haxe
function generateManifest(assets:Array<AssetInfo>):AssetManifest {
var manifest = new AssetManifest();
for (asset in assets) {
manifest.addAsset(
asset.id,
asset.path,
asset.type,
asset.parameters
);
}
return manifest;
}
```
### 4.2 Runtime Access Layer Implementation
1. **Assets Class**:
- Create a static API similar to OpenFL's Assets class
- Implement type-specific getters (getBitmapData, getText, etc.)
- Support both synchronous and asynchronous loading
```haxe
class Assets {
private static var libraries:Map<String, AssetLibrary> = new Map();
private static var cache:AssetCache = new AssetCache();
public static function registerLibrary(name:String, library:AssetLibrary):Void {
libraries.set(name, library);
}
public static function getBitmapData(id:String, useCache:Bool = true):BitmapData {
// Implementation
}
public static function loadBitmapData(id:String):Future<BitmapData> {
// Async implementation
}
// Other asset type methods
}
```
2. **Asset Library**:
- Implement a library class to manage asset collections
- Support manifest loading
- Handle asset type detection and loading
```haxe
class AssetLibrary {
private var assets:Map<String, AssetInfo> = new Map();
public static function fromManifest(manifest:AssetManifest):AssetLibrary {
var library = new AssetLibrary();
for (asset in manifest.assets) {
library.addAsset(asset.id, asset);
}
return library;
}
public function getBitmapData(id:String):BitmapData {
var asset = assets.get(id);
if (asset != null && asset.type == AssetType.IMAGE) {
return loadBitmapDataSync(asset.path);
}
return null;
}
// Other loading methods
}
```
### 4.3 Resource Management Implementation
1. **Asset Cache**:
- Implement a multi-type caching system
- Support cache control operations
- Add memory usage tracking
```haxe
class AssetCache {
private var bitmapData:Map<String, BitmapData> = new Map();
private var text:Map<String, String> = new Map();
// Other asset type caches
public function has(id:String, type:AssetType):Bool {
switch (type) {
case AssetType.IMAGE:
return bitmapData.exists(id);
// Other types
}
return false;
}
public function set(id:String, asset:Dynamic, type:AssetType):Void {
// Implementation
}
public function get(id:String, type:AssetType):Dynamic {
// Implementation
}
public function clear(type:AssetType = null):Void {
// Implementation
}
}
```
2. **Memory Management**:
- Implement reference counting for disposable assets
- Add cache size limits
- Create cache eviction policies
```haxe
class MemoryMonitor {
private static var memoryUsage:Map<String, Int> = new Map();
private static var maxMemory:Int = 100 * 1024 * 1024; // 100MB example limit
public static function trackAsset(id:String, size:Int):Void {
memoryUsage.set(id, size);
enforceMemoryLimits();
}
public static function untrackAsset(id:String):Void {
memoryUsage.remove(id);
}
private static function enforceMemoryLimits():Void {
var totalUsage = calculateTotalUsage();
if (totalUsage > maxMemory) {
evictCache(totalUsage - maxMemory);
}
}
private static function evictCache(bytesToFree:Int):Void {
// Implement LRU or other eviction strategy
}
}
```
## 5. Critical Implementation Considerations
When implementing your custom asset system, keep these factors in mind:
### Performance Optimization
- Implement asynchronous loading for large assets
- Use streaming for audio/video when appropriate
- Implement progressive loading for large textures
### Platform Compatibility
- Handle platform-specific file format requirements
- Implement fallback loading mechanisms
- Consider thread safety for multi-threaded platforms
### Memory Management
- Monitor memory usage of cached assets
- Implement automatic cache cleanup
- Provide manual unloading API for memory-constrained environments
### Asset Versioning
- Implement asset versioning to handle updates
- Consider cache invalidation strategies
- Support hot-reloading during development
By following these implementation guidelines, you can create a robust asset management system that provides functionality similar to OpenFL's built-in system while giving you complete control over the asset bundling and loading process.

View File

@ -1,13 +1,29 @@
-cp src
-lib lime
-lib openfl
-D lime-default
-D openfl-legacy
-D native
-D lime-cffi
-D lime-native
-D tools
# Run asset macro to build the manifest
--macro macros.AssetMacro.buildAssets()
# Entry point
-main ApplicationMain -main ApplicationMain
-lib openfl # C++ target - adjust as needed
-lib lime -cpp bin/cpp
# Debug mode
-debug
-lib hxcpp -lib hxcpp
-lib actuate -lib actuate
-lib hxp -lib hxp
-cp src
-cp assets -cp assets
-D openfl=8.9.5 -D openfl=8.9.5
-D lime=7.9.0 -D lime=7.9.0
@ -32,6 +48,4 @@
-D vsync=true -D vsync=true
-D depthBuffer=true -D depthBuffer=true
-D stencilBuffer=false -D stencilBuffer=false
-D parameters="{}" -D parameters="{}"
--cpp bin/cpp

View File

@ -0,0 +1,7 @@
src/macros/AssetMacro.hx:106: Added asset: data/img/placeholder.txt
src/macros/AssetMacro.hx:48: DEBUG - JSON starts with: {
"name": "default",
"rootPath": "../",
"ver...
src/macros/AssetMacro.hx:55: Asset manifest saved to: bin/cpp/manifest/default.json
src/macros/AssetMacro.hx:55: Asset manifest saved to: manifest/default.json

9
standalone-openfl-app/compile.sh Normal file → Executable file
View File

@ -1,8 +1,5 @@
#!/bin/bash #!/bin/bash
# Navigate to the project directory
cd "$(dirname "$0")"
# Compile the project using the Haxe compiler with the specified build.hxml configuration # Compile the project using the Haxe compiler with the specified build.hxml configuration
haxe build.hxml haxe build.hxml
@ -11,4 +8,8 @@ if [ $? -eq 0 ]; then
echo "Compilation successful!" echo "Compilation successful!"
else else
echo "Compilation failed!" echo "Compilation failed!"
fi fi
# Run the compiled project
# We need to run ApplicationMain-debug within the bin/cpp directory as the current working directory
(cd ./bin/cpp; ./ApplicationMain-debug)

View File

@ -0,0 +1,14 @@
{
"name": "default",
"libraryType": null,
"rootPath": ".",
"assets": [
{
"path": "assets/data/img/placeholder.txt",
"id": "data/img/placeholder.txt",
"type": "text"
}
],
"version": 2,
"libraryArgs": []
}

View File

@ -1,136 +1,63 @@
package; package;
import openfl.Lib; import openfl.Lib;
import lime.utils.AssetType;
import lime.tools.Asset;
import lime.ui.WindowAttributes; import lime.ui.WindowAttributes;
import openfl.system.System;
import openfl.display.Application; import openfl.display.Application;
import openfl.display.Stage; import openfl.display.Stage;
import openfl.events.Event; import openfl.events.Event;
import openfl.events.FullScreenEvent;
@:access(lime.app.Application) @:access(lime.app.Application)
@:access(lime.system.System) @:access(lime.system.System)
@:access(openfl.display.Stage) @:access(openfl.display.Stage)
@:access(openfl.events.UncaughtErrorEvents) @:access(openfl.events.UncaughtErrorEvents)
class ApplicationMain { class ApplicationMain {
public static function main() { public static function main() {
lime.system.System.__registerEntryPoint("src/DocumentClass.hx", create); @:privateAccess lime.system.System.__registerEntryPoint("src/Main.hx", create);
create(null); create(null);
} }
public static function create(config):Void { public static function create(config):Void {
try { try {
trace("Creating minimal application"); trace("Creating application");
var app = new Application(); var app = new Application();
// Initialize assets via macro-generated code // Initialize assets via custom loader
Assets.initializeAssets(); try {
trace("Assets initialized"); Assets.initializeAssets();
trace("Assets initialized");
} catch (e:Dynamic) {
trace("Error initializing assets: " + e);
}
// Minimal metadata // Minimal metadata
app.meta["name"] = "Debug Application"; app.meta["name"] = "DSTEngine Demo";
// Simple window // Simple window
var attributes:WindowAttributes = { var attributes:WindowAttributes = {
height: 600, height: 600,
width: 800, width: 800,
title: "Debug Window" title: "DST Engine Demo"
}; };
app.createWindow(attributes); app.createWindow(attributes);
// Skip preloader, just call start directly
start(app.window.stage);
var result = app.exec(); // Start application
lime.system.System.exit(result); start(app.window.stage);
} catch (e:Dynamic) {
trace("Error: " + e); var result = app.exec();
} lime.system.System.exit(result);
} } catch (e:Dynamic) {
trace("Error creating application: " + e);
trace(haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
}
}
// public static function create(config):Void public static function start(stage:Stage):Void {
// { try {
// var app = new Application(); var main = new Main();
// app.meta["build"] = "1.0.0"; // Replace with actual build number stage.addChild(main);
// app.meta["company"] = "Your Company"; // Replace with your company name } catch (e:Dynamic) {
// app.meta["file"] = "src/Main.hx"; // Path to your main application file trace("Error starting application: " + e);
// app.meta["name"] = "Your Application"; // Replace with your application title }
// app.meta["packageName"] = "com.yourcompany.yourapp"; // Replace with your package name }
// app.meta["version"] = "1.0.0"; // Replace with your application version
// // var asset = new Asset();
// // asset.sourcePath = "src/assets"; // Path to your assets
// // asset.targetPath = "assets"; // Target path for assets
// // asset.type = AssetType.IMAGE; // Type of asset
// if (config.hxtelemetry != null)
// {
// app.meta["hxtelemetry-allocations"] = config.hxtelemetry.allocations;
// app.meta["hxtelemetry-host"] = config.hxtelemetry.host;
// }
// var attributes:WindowAttributes = {
// allowHighDPI: true, // Set to true or false as needed
// alwaysOnTop: false, // Set to true or false as needed
// borderless: false, // Set to true or false as needed
// frameRate: 60, // Set your desired frame rate
// height: 600, // Set your desired window height
// hidden: false, // Set to true or false as needed
// maximized: false, // Set to true or false as needed
// minimized: false, // Set to true or false as needed
// resizable: true, // Set to true or false as needed
// title: "Your Application", // Replace with your application title
// width: 800, // Set your desired window width
// x: 100, // Set your desired x position
// y: 100 // Set your desired y position
// };
// app.createWindow(attributes);
// var preloader = getPreloader();
// app.preloader.onProgress.add(function(loaded, total)
// {
// @:privateAccess preloader.update(loaded, total);
// });
// app.preloader.onComplete.add(function()
// {
// @:privateAccess preloader.start();
// });
// preloader.onComplete.add(start.bind(app.window.stage));
// app.preloader.load();
// var result = app.exec();
// lime.system.System.exit(result);
// }
public static function start(stage:Stage):Void {
if (stage.__uncaughtErrorEvents.__enabled) {
try {
// Instantiate and add DocumentClass
var documentClass = new DocumentClass();
Lib.current.addChild(documentClass);
// Then dispatch events
stage.dispatchEvent(new Event(Event.RESIZE, false, false));
if (stage.window.fullscreen) {
stage.dispatchEvent(new FullScreenEvent(FullScreenEvent.FULL_SCREEN, false, false, true, true));
}
} catch (e:Dynamic) {
stage.__handleError(e);
}
} else {
// Instantiate and add DocumentClass
var documentClass = new DocumentClass();
Lib.current.addChild(documentClass);
// Then dispatch events
stage.dispatchEvent(new Event(Event.RESIZE, false, false));
if (stage.window.fullscreen) {
stage.dispatchEvent(new FullScreenEvent(FullScreenEvent.FULL_SCREEN, false, false, true, true));
}
}
}
public static function getPreloader() {
return new openfl.display.Preloader();
}
} }

View File

@ -1,32 +1,261 @@
package; package;
import openfl.display.BitmapData; import haxe.io.Path;
import openfl.media.Sound; import openfl.utils.AssetLibrary;
import openfl.text.Font; import openfl.utils.AssetManifest;
import openfl.utils.ByteArray; import openfl.utils.AssetType;
import openfl.events.Event;
import openfl.events.EventDispatcher;
import sys.FileSystem;
import sys.io.File;
import haxe.crypto.Base64;
@:build(macros.AssetMacro.buildAssets()) /**
* Custom asset management system that initializes OpenFL's asset system.
* This supplements OpenFL's built-in asset handling.
*/
class Assets { class Assets {
// The @:build macro will inject the initializeAssets method // Track initialization state
public static var isLoaded:Bool = false;
// Helper methods for convenience // Event dispatcher for asset loading events
public static function getBitmapData(id:String):BitmapData { private static var dispatcher:EventDispatcher = new EventDispatcher();
return openfl.Assets.getBitmapData(id);
// Potential manifest locations
private static var manifestPaths = [
"manifest/default.json",
"bin/cpp/manifest/default.json",
"../manifest/default.json"
];
// Event types
public static inline var ASSET_LOADED:String = "assetLoaded";
public static inline var ASSET_ERROR:String = "assetError";
/**
* Initialize the asset system
*/
public static function initializeAssets():Void {
if (isLoaded) return; // Don't load twice
trace("Initializing asset system...");
// First, try to initialize embedded assets if the macro created them
try {
if (Reflect.hasField(macros.AssetMacro, "initEmbeddedAssets")) {
trace("Initializing embedded assets...");
Reflect.callMethod(macros.AssetMacro,
Reflect.field(macros.AssetMacro, "initEmbeddedAssets"), []);
isLoaded = true;
}
} catch (e:Dynamic) {
trace("No embedded assets available: " + e);
}
// Then try to load from manifest files
loadFromManifest();
if (isLoaded) {
trace("Asset system initialized successfully.");
// Log available assets
trace("Available assets:");
for (assetPath in openfl.Assets.list()) {
var type = getAssetTypeString(assetPath);
trace(" - " + assetPath + " (Type: " + type + ")");
}
} else {
trace("WARNING: Asset system could not be fully initialized.");
}
} }
public static function getSound(id:String):Sound { /**
return openfl.Assets.getSound(id); * Try to load assets from an external manifest file
*/
private static function loadFromManifest():Void {
trace("Checking for asset manifest files...");
for (path in manifestPaths) {
if (FileSystem.exists(path)) {
try {
trace("Loading asset manifest from: " + path);
var content = File.getContent(path);
var jsonStartPos = 0;
while (jsonStartPos < content.length && content.charAt(jsonStartPos) != '{') {
jsonStartPos++;
}
if (jsonStartPos > 0) {
content = content.substr(jsonStartPos);
}
if (content == null || content.length == 0 || content.charAt(0) != '{') {
trace("Invalid JSON content in manifest file");
continue;
}
var manifestData = haxe.Json.parse(content);
var manifest = new AssetManifest();
manifest.name = manifestData.name;
manifest.assets = manifestData.assets;
manifest.rootPath = manifestData.rootPath != null ? manifestData.rootPath : Path.directory(path);
manifest.version = manifestData.version;
manifest.libraryType = manifestData.libraryType;
manifest.libraryArgs = manifestData.libraryArgs;
if (manifest.assets != null && manifest.assets.length > 0) {
trace("Parsed manifest with " + manifest.assets.length + " assets");
var library = AssetLibrary.fromManifest(manifest);
if (library != null) {
var libraryName = manifest.name != null ? manifest.name : "default";
if (openfl.Assets.hasLibrary(libraryName)) {
openfl.Assets.unloadLibrary(libraryName);
}
openfl.Assets.registerLibrary(libraryName, library);
trace("Registered asset library: " + libraryName);
isLoaded = true;
dispatcher.dispatchEvent(new Event(ASSET_LOADED));
return;
} else {
trace("ERROR: Failed to create library from manifest");
}
} else {
trace("ERROR: No assets found in manifest");
}
} catch (e:Dynamic) {
trace("Error loading manifest from " + path + ": " + e);
}
} else {
trace("Manifest not found at: " + path);
}
}
dispatcher.dispatchEvent(new Event(ASSET_ERROR));
} }
public static function getFont(id:String):Font { /**
return openfl.Assets.getFont(id); * Add event listener for asset events
*/
public static function addEventListener(type:String, listener:Dynamic->Void,
useCapture:Bool = false, priority:Int = 0,
useWeakReference:Bool = false):Void {
dispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference);
} }
public static function getText(id:String):String { /**
return openfl.Assets.getText(id); * Remove event listener
*/
public static function removeEventListener(type:String, listener:Dynamic->Void,
useCapture:Bool = false):Void {
dispatcher.removeEventListener(type, listener, useCapture);
} }
public static function getBytes(id:String):ByteArray { /**
return openfl.Assets.getBytes(id); * Force reload the asset system
*/
public static function reload():Void {
var libraryNames = openfl.Assets.list().map(function(id) {
var parts = id.split(":");
return parts.length > 1 ? parts[0] : "default";
}).filter(function(name) {
return name != null && name != "";
});
var uniqueNames = new Map<String, Bool>();
for (name in libraryNames) {
uniqueNames.set(name, true);
}
for (name in uniqueNames.keys()) {
if (openfl.Assets.hasLibrary(name)) {
try {
openfl.Assets.unloadLibrary(name);
} catch (e:Dynamic) {
trace("Error unloading library " + name + ": " + e);
}
}
}
isLoaded = false;
initializeAssets();
}
/**
* Helper function to determine asset types by extension
*/
private static function getAssetTypeString(id:String):String {
try {
if (!openfl.Assets.exists(id)) return "unknown";
var extension = Path.extension(id).toLowerCase();
return switch(extension) {
case "jpg", "jpeg", "png", "gif", "bmp": "image";
case "mp3", "ogg", "wav": "sound";
case "ttf", "otf": "font";
case "txt", "json", "xml", "csv", "tsv": "text";
default: "binary";
}
} catch (e:Dynamic) {
return "unknown";
}
}
/**
* Debug the manifest and asset system status
*/
public static function debugManifest():String {
var result = "Asset System Status:\n";
result += "Initialized: " + isLoaded + "\n\n";
var hasManifest = false;
for (path in manifestPaths) {
if (FileSystem.exists(path)) {
hasManifest = true;
try {
var content = File.getContent(path);
var jsonStartPos = 0;
while (jsonStartPos < content.length && content.charAt(jsonStartPos) != '{') {
jsonStartPos++;
}
if (jsonStartPos > 0) {
content = content.substr(jsonStartPos);
}
result += "Manifest: " + path + "\n";
result += "Size: " + content.length + " bytes\n";
if (content.length > 200) {
result += "Content: " + content.substr(0, 200) + "...\n";
} else {
result += "Content: " + content + "\n";
}
var assetCount = "unknown";
try {
var jsonData = haxe.Json.parse(content);
if (jsonData != null && jsonData.assets != null) {
assetCount = Std.string(jsonData.assets.length);
}
} catch (e:Dynamic) {
assetCount = "Error: " + e;
}
result += "Assets: " + assetCount + "\n";
} catch (e:Dynamic) {
result += "Error reading manifest " + path + ": " + e + "\n";
}
}
}
if (!hasManifest) {
result += "No manifest files found.\n";
}
var libraryNames = [];
try {
for (asset in openfl.Assets.list()) {
var libName = "";
var colonIndex = asset.indexOf(":");
if (colonIndex > -1) {
libName = asset.substring(0, colonIndex);
} else {
libName = "default";
}
if (libraryNames.indexOf(libName) == -1) {
libraryNames.push(libName);
}
}
} catch (e:Dynamic) {
result += "Error getting libraries: " + e + "\n";
}
result += "\nLibraries: " + libraryNames.join(", ") + "\n";
result += "Asset count: " + openfl.Assets.list().length;
return result;
} }
} }

View File

@ -0,0 +1,87 @@
package;
import haxe.io.Bytes;
import openfl.display.BitmapData;
import openfl.media.Sound;
import openfl.utils.AssetLibrary;
import openfl.utils.AssetType;
import openfl.utils.Future;
import openfl.display.Loader;
import openfl.events.Event;
import openfl.utils.ByteArray;
class EmbeddedAssetLibrary extends AssetLibrary {
private var byteData:Map<String, Bytes>;
private var texts:Map<String, String>;
private var images:Map<String, BitmapData>;
private var sounds:Map<String, Sound>;
public function new(byteData:Map<String, Bytes>, texts:Map<String, String>,
images:Map<String, BitmapData>, sounds:Map<String, Sound>) {
super();
this.byteData = byteData;
this.texts = texts;
this.images = images;
this.sounds = sounds;
}
override public function exists(id:String, type:AssetType = null):Bool {
switch (type) {
case AssetType.BINARY:
return byteData.exists(id);
case AssetType.TEXT:
return texts.exists(id);
case AssetType.IMAGE:
return images.exists(id);
case AssetType.SOUND:
return sounds.exists(id);
default:
return byteData.exists(id) || texts.exists(id) || images.exists(id) || sounds.exists(id);
}
}
override public function getType(id:String):AssetType {
if (byteData.exists(id)) return AssetType.BINARY;
if (texts.exists(id)) return AssetType.TEXT;
if (images.exists(id)) return AssetType.IMAGE;
if (sounds.exists(id)) return AssetType.SOUND;
return null;
}
override public function getText(id:String):String {
return texts.get(id);
}
override public function getBytes(id:String):Bytes {
return byteData.get(id);
}
override public function getBitmapData(id:String):BitmapData {
return images.get(id);
}
override public function loadBitmapData(id:String):Future<BitmapData> {
var future = new Future<BitmapData>();
if (images.exists(id)) {
future.onComplete(images.get(id));
} else {
loadBitmapDataAsync(id);
}
return future;
}
private function loadBitmapDataAsync(id:String):Void {
if (byteData.exists(id)) {
var loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e) {
var bitmapData = cast(loader.content, BitmapData);
images.set(id, bitmapData);
});
loader.loadBytes(ByteArray.fromBytes(byteData.get(id)));
}
}
override public function getSound(id:String):Sound {
return sounds.get(id);
}
}

View File

@ -1,19 +1,117 @@
package; package;
import openfl.events.EventDispatcher;
import openfl.display.Sprite; import openfl.display.Sprite;
import openfl.events.Event; import openfl.events.Event;
import openfl.display.DisplayObjectContainer; import openfl.Assets;
import openfl.events.MouseEvent; import openfl.display.Bitmap;
import openfl.display.BitmapData;
import openfl.text.TextField;
import openfl.text.TextFormat;
class Main extends Sprite class Main extends Sprite {
{ public function new() {
public function new()
{
super(); super();
dispatchEvent(new openfl.events.Event(openfl.events.Event.ADDED_TO_STAGE, false, false));
graphics.beginFill(0xFFF00F, 1); // Draw background to verify app is running
graphics.beginFill(0x3498db, 1);
graphics.drawRect(0, 0, 800, 600); graphics.drawRect(0, 0, 800, 600);
graphics.endFill(); graphics.endFill();
// Create title
var title = new TextField();
title.defaultTextFormat = new TextFormat("Arial", 24, 0xFFFFFF, true);
title.text = "OpenFL Asset Test";
title.width = 400;
title.x = 20;
title.y = 20;
addChild(title);
// Display info about available assets
displayAssetInfo();
// Try to load and display a text asset
loadTextAsset();
// Debug manifest content
debugManifestInfo();
} }
}
private function displayAssetInfo():Void {
var info = new TextField();
info.defaultTextFormat = new TextFormat("Arial", 16, 0xFFFFFF);
info.width = 760;
info.height = 300;
info.x = 20;
info.y = 60;
info.multiline = true;
info.wordWrap = true;
var assetList = openfl.Assets.list();
info.text = "Available Assets (" + assetList.length + "):\n";
for (asset in assetList) {
var extension = asset.substr(asset.lastIndexOf(".") + 1).toLowerCase();
var typeStr = getAssetTypeFromExtension(extension);
info.text += " " + asset + " [" + typeStr + "]\n";
}
addChild(info);
}
private function getAssetTypeFromExtension(extension:String):String {
return switch(extension) {
case "jpg", "jpeg", "png", "gif", "bmp": "IMAGE";
case "mp3", "ogg", "wav": "SOUND";
case "ttf", "otf": "FONT";
case "txt", "json", "xml", "csv", "tsv": "TEXT";
default: "BINARY";
};
}
private function loadTextAsset():Void {
var textDisplay = new TextField();
textDisplay.defaultTextFormat = new TextFormat("Arial", 14, 0xFFFFFF);
textDisplay.width = 760;
textDisplay.height = 200;
textDisplay.x = 20;
textDisplay.y = 380;
textDisplay.multiline = true;
textDisplay.wordWrap = true;
textDisplay.border = true;
textDisplay.borderColor = 0xFFFFFF;
textDisplay.background = true;
textDisplay.backgroundColor = 0x333333;
var textPath = "data/img/placeholder.txt";
if (Assets.exists(textPath)) {
// Synchronous loading
var content = Assets.getText(textPath);
textDisplay.text = "Loaded text content from '" + textPath + "':\n\n" + content;
} else {
// Try alternative paths
var altPath = "assets/data/img/placeholder.txt";
if (Assets.exists(altPath)) {
var content = Assets.getText(altPath);
textDisplay.text = "Loaded text content from '" + altPath + "':\n\n" + content;
} else {
textDisplay.text = "Could not find text asset '" + textPath + "' or '" + altPath + "'";
}
}
addChild(textDisplay);
}
private function debugManifestInfo():Void {
var debug = new TextField();
debug.defaultTextFormat = new TextFormat("Arial", 12, 0xFF0000);
debug.width = 760;
debug.height = 100;
debug.x = 20;
debug.y = 540;
debug.multiline = true;
debug.wordWrap = true;
debug.text = Assets.debugManifest();
addChild(debug);
}
}

View File

@ -4,109 +4,261 @@ import haxe.macro.Context;
import haxe.macro.Expr; import haxe.macro.Expr;
import sys.FileSystem; import sys.FileSystem;
import sys.io.File; import sys.io.File;
import haxe.io.Path;
#if macro
import haxe.Json; import haxe.Json;
import haxe.crypto.Base64;
import haxe.format.JsonPrinter;
#end
class AssetMacro { class AssetMacro {
public static macro function buildAssets():Array<Field> { public static macro function buildAssets():Array<Field> {
var fields = Context.getBuildFields(); var fields = Context.getBuildFields();
// Define assets directory #if !display
var assetsDir = "assets";
// Check if directory exists
if (!FileSystem.exists(assetsDir) || !FileSystem.isDirectory(assetsDir)) {
Context.warning('Assets directory "$assetsDir" not found', Context.currentPos());
return fields;
}
// Generate manifest in the format OpenFL expects
var manifest = {
name: "default",
rootPath: "",
assets: []
};
// Scan assets
scanDirectory(assetsDir, manifest.assets);
// Create manifest file (for runtime use)
var manifestPath = "bin/manifest.json";
try { try {
// Make sure directory exists // Define output directories
var dir = manifestPath.split("/")[0]; var targetBuildDir = "bin/cpp";
if (!FileSystem.exists(dir)) { var manifestDir = targetBuildDir + "/manifest";
FileSystem.createDirectory(dir); var assetsOutputDir = targetBuildDir + "/assets";
// Create directories if they don't exist
ensureDirectory(manifestDir);
ensureDirectory(assetsOutputDir);
ensureDirectory("manifest");
// Add a test placeholder if no assets exist
if (!FileSystem.exists("assets")) {
ensureDirectory("assets/data/img");
File.saveContent("assets/data/img/placeholder.txt", "This is a placeholder file.");
} }
File.saveContent(manifestPath, Json.stringify(manifest, null, " "));
} catch (e) { // Scan for assets and build asset list
Context.warning('Could not save manifest file: $e', Context.currentPos()); var assets = [];
} var assetDir = "assets";
var embeddedAssets = new Map<String, {data: String, type: String}>();
// Generate expression to register the assets
var registerExpr = macro { if (FileSystem.exists(assetDir)) {
// Create an AssetManifest from our generated file scanAssets(assetDir, assets, "", assetsOutputDir, embeddedAssets);
var manifestPath = "bin/manifest.json"; trace('Found ${assets.length} assets in ${assetDir}');
if (sys.FileSystem.exists(manifestPath)) {
var manifest = lime.utils.AssetManifest.fromFile(manifestPath);
if (manifest != null) {
// Register the library with OpenFL
var library = openfl.utils.AssetLibrary.fromManifest(manifest);
if (library != null) {
openfl.Assets.registerLibrary("default", library);
trace("Asset library registered successfully");
} else {
trace("Failed to create library from manifest");
}
} else {
trace("Failed to parse manifest file");
}
} else { } else {
trace("Manifest file not found at: " + manifestPath); trace('WARNING: Assets directory not found: ${assetDir}');
} }
};
// Create a default asset if none found
// Add initialization method if (assets.length == 0) {
fields.push({ var placeholder = {
name: "initializeAssets", path: "assets/data/img/placeholder.txt",
access: [Access.APublic, Access.AStatic], type: "TEXT",
kind: FieldType.FFun({ id: "data/img/placeholder.txt"
args: [], };
ret: macro:Void, assets.push(placeholder);
expr: registerExpr var placeholderContent = "This is a placeholder file.";
}), embeddedAssets.set(placeholder.id, {
pos: Context.currentPos() data: Base64.encode(haxe.io.Bytes.ofString(placeholderContent)),
}); type: "TEXT"
});
}
// Create manifest in OpenFL format
var manifest = {
name: "default",
assets: assets.map(function(asset) {
return {
id: asset.id,
path: asset.path,
type: asset.type.toLowerCase()
};
}),
rootPath: ".",
version: 2,
libraryType: null,
libraryArgs: []
};
var manifestJson = haxe.format.JsonPrinter.print(manifest, null, " ");
File.saveContent(manifestDir + "/default.json", manifestJson);
trace('Asset manifest saved to: ' + manifestDir + "/default.json");
File.saveContent("manifest/default.json", manifestJson);
trace('Asset manifest also saved to: manifest/default.json');
// Generate the embedded assets code
if (embeddedAssets.keys().hasNext()) {
var initExprs = [];
initExprs.push(macro var assetLibrary = new CustomAssetLibrary());
for (id in embeddedAssets.keys()) {
var asset = embeddedAssets.get(id);
if (asset == null) continue;
switch (asset.type) {
case "TEXT":
var dataExpr = macro haxe.crypto.Base64.decode($v{asset.data}).getString(0, haxe.crypto.Base64.decode($v{asset.data}).length);
initExprs.push(macro assetLibrary.registerAsset($v{id}, "text", $dataExpr));
case "IMAGE":
initExprs.push(macro {
var bytes = haxe.crypto.Base64.decode($v{asset.data});
assetLibrary.registerAsset($v{id}, "image", bytes);
});
case "SOUND":
initExprs.push(macro {
var bytes = haxe.crypto.Base64.decode($v{asset.data});
assetLibrary.registerAsset($v{id}, "sound", bytes);
});
default:
var dataExpr = macro haxe.crypto.Base64.decode($v{asset.data});
initExprs.push(macro assetLibrary.registerAsset($v{id}, "binary", $dataExpr));
}
}
initExprs.push(macro openfl.Assets.registerLibrary("default", assetLibrary));
fields.push({
name: "initEmbeddedAssets",
doc: "Initializes embedded assets for the application",
meta: [],
access: [Access.APublic, Access.AStatic],
kind: FieldType.FFun({
args: [],
expr: macro $b{initExprs},
params: [],
ret: macro :Void
}),
pos: Context.currentPos()
});
}
} catch (e:Dynamic) {
trace('ERROR in asset macro: ${e}');
#if debug
trace(haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
#end
}
#end
return fields; return fields;
} }
private static function scanDirectory(dir:String, assets:Array<Dynamic>, ?prefix:String = ""):Void { #if macro
for (file in FileSystem.readDirectory(dir)) { private static function ensureDirectory(dir:String):Void {
var path = dir + "/" + file; if (!FileSystem.exists(dir)) {
var id = prefix + file; try {
var pathParts = dir.split("/");
if (FileSystem.isDirectory(path)) { var currentPath = "";
scanDirectory(path, assets, id + "/");
} else { for (part in pathParts) {
var type = getAssetType(file); if (part == "") continue;
assets.push({ currentPath += part + "/";
id: id, if (!FileSystem.exists(currentPath)) {
path: path, FileSystem.createDirectory(currentPath);
type: type }
}); }
} catch (e:Dynamic) {
trace('ERROR creating directory ${dir}: ${e}');
} }
} }
} }
private static function getAssetType(file:String):String { private static function scanAssets(dir:String, assets:Array<Dynamic>, relativePath:String = "",
var ext = file.substr(file.lastIndexOf(".") + 1).toLowerCase(); outputDir:String = null, embeddedAssets:Map<String, {data:String, type:String}>):Void {
if (!FileSystem.exists(dir)) return;
return switch (ext) { try {
case "jpg", "jpeg", "png", "gif", "bmp": "image"; var files = FileSystem.readDirectory(dir);
case "mp3", "ogg", "wav": "sound";
case "ttf", "otf": "font"; for (file in files) {
case "txt", "json", "xml", "csv": "text"; try {
default: "binary"; var path = dir + "/" + file;
var relPath = relativePath == "" ? file : relativePath + "/" + file;
if (FileSystem.isDirectory(path)) {
// Create the corresponding directory in output
if (outputDir != null) {
ensureDirectory(outputDir + "/" + relPath);
}
// Recursive scan subdirectories
scanAssets(path, assets, relPath, outputDir, embeddedAssets);
} else {
// Determine asset type based on extension
var extension = Path.extension(file).toLowerCase();
var type = getAssetType(extension);
// Skip hidden files and non-asset files
if (file.charAt(0) == "." || type == "UNKNOWN") continue;
// Copy the file to output directory
if (outputDir != null) {
var outputPath = outputDir + "/" + relPath;
try {
File.copy(path, outputPath);
trace('Copied asset: ${path} to ${outputPath}');
} catch (e:Dynamic) {
trace('Error copying ${path} to ${outputPath}: ${e}');
}
}
// Add to asset list with proper path format
var assetId = relPath.split("\\").join("/"); // Normalize path separators
var assetPath = "assets/" + assetId;
assets.push({
path: assetPath,
type: type,
id: assetId
});
// Embed the asset content for runtime access
try {
var content = File.getBytes(path);
embeddedAssets.set(assetId, {
data: Base64.encode(content),
type: type
});
} catch (e:Dynamic) {
trace('Warning: Could not embed asset ${assetId}: ${e}');
}
trace('Added asset: ${assetId}');
}
} catch (innerE:Dynamic) {
trace('Error processing asset ${file}: ${innerE}');
}
}
} catch (e:Dynamic) {
trace('Error reading directory ${dir}: ${e}');
} }
} }
private static function getAssetType(extension:String):String {
return switch(extension) {
case "jpg", "jpeg", "png", "gif", "bmp":
"IMAGE";
case "mp3", "ogg", "wav":
"SOUND";
case "ttf", "otf":
"FONT";
case "txt", "json", "xml", "csv", "tsv", "frag", "vert", "properties":
"TEXT";
case "bytes", "bin":
"BINARY";
default:
"TEXT"; // Default to TEXT for unknown types
}
}
private static function generateEmbeddedAssetExprs(embeddedAssets:Map<String, {data:String, type:String}>):Array<Expr> {
var exprs:Array<Expr> = [];
for (id in embeddedAssets.keys()) {
var asset = embeddedAssets.get(id);
if (asset == null) continue;
var dataExpr = macro haxe.crypto.Base64.decode($v{asset.data});
switch (asset.type) {
case "TEXT":
exprs.push(macro texts.set($v{id}, dataExpr.getString(0, dataExpr.length)));
case "IMAGE":
exprs.push(macro byteData.set($v{id}, dataExpr));
case "SOUND":
exprs.push(macro byteData.set($v{id}, dataExpr));
default:
exprs.push(macro byteData.set($v{id}, dataExpr));
}
}
return exprs;
}
#end
} }