tutorial documentation

This commit is contained in:
2025-02-08 17:09:34 -06:00
parent c389e0744a
commit 4361ad9daa
19 changed files with 432 additions and 299 deletions

View File

@@ -1,11 +1,13 @@
nav:
- index.md
- dull.md
- tutorial.md
- actors.md
- rendering.md
- resources.md
- input.md
- exporting.md
- dull.md
- prosperon.md
- ...
- api

View File

@@ -1,235 +1,117 @@
#Actor & Module System
# Programs: Actors and Modules
• main.js is the first program started by the engine. Once main.js ends (is killed), the engine exits
(equivalent to clicking the windows “X”).
• Files that do not return anything (i.e. do not include a “return …;” statement) are considered programs.
• Files that do return a single object are modules, imported via the use('filename') function.
• Actors are running program objects. They can spawn new actors as underlings, with the spawner acting as
their overling.
Prosperon organizes your code into two broad categories: **modules** and **actors**. This page explains how they work, how they fit together, and some of their unique quirks. Well also touch on **doc.js**, which uses special docstring notation to automatically generate documentation for your modules, actors, and C types.
Modules
-------
You can import a module by calling:
use('some_module')
This function searches through the engines path list (prosperon.PATH) for files such as some_module.js or
some_module.jsc (among other supported extensions). If found, the module portion is executed once and its
return value is cached; subsequent calls to use(...) with the same name return the cached object.
---
Key rules:
1. A module must end with a “return something;” statement.
2. The value returned by a module is typically an object containing functions or configuration.
3. A module is loaded at most once per engine run; later use(...) calls return the same module object.
## Modules
Example (config.js):
---------------------
// config.js
---
return {
width: 800,
height: 600
}
A **module** is any file that returns a single value. This return value is commonly an object, but it can be any data type (string, number, function, etc.). Once a module returns its value, Prosperon **freezes** that value, preventing accidental modification. The module is then cached so that subsequent imports of the same module dont re-run the file—they reuse the cached result.
In your program you might write:
var config = use('config.js')
console.log(config.width) // Outputs: 800
### Importing a Module
Programs
--------
Prosperon distinguishes programs from modules by whether they return a value:
Use the built-in `use` function to import a module by file path (or by name if resolvable via Prosperons path settings). For example:
• If a file does not include a “return …;” statement, it is considered a program.
• A program can be spawned as an actor.
• main.js is the special “root” program; when it terminates (or is killed), the engine quits.
```
var myModule = use('scripts/modules/myModule')
```
Mixed Files (Modules + Programs)
----------------------------------
A single file can contain both module code and program code if the two sections are separated by a line
containing just:
---
If the first part includes a “return …;” statement, it is treated as the module portion, while the second
part is treated as the program portion. This design allows one file to serve as both a module (exposing an API)
and an actor script.
`use('myModule')` returns the **exact** same object if called multiple times, since modules are cached and not re-run.
Example structure in one file:
--------------------------------
// Module portion
return {
someUtility() { /* … */ }
}
---
// Program portion
this.start = function() {
console.log("Hello from the actor part!");
}
Dull based modules are resolved by searching for them from the `prosperon.PATH` array. Engine modules are stored under `scripts/modules`, which is already added to the PATH for you.
The engine will parse and load the module portion when use('file') is called; if the file is spawned as a
program, the program portion is executed on the actor.
Prosperon can also load C based modules. If two modules have the same path resolution, the C based library will be imported.
Actor System
------------
Creating Actors:
----------------
Any running actor can create an underling by calling its built-in spawn method. For example, in main.js
(the root program):
## Actors
// In main.js
var child = this.spawn('child.js', { name: "ChildActor" })
An **actor** is created from a file that **does not** return a value. Instead, the files contents run top to bottom as soon as the actor is spawned. Actors are your games “live” scripts: each actor can hold its own state and logic, spawn sub-actors, schedule timed tasks, and eventually **kill** itself (or be killed) when its done.
spawn accepts the following arguments:
1. script (string): The file name or path to another program file.
2. config (optional object): Properties to merge into the spawned actor.
3. callback (optional function): Invoked immediately after creation with (underling, info).
### Actor Intrinsic Functions
The spawned actors “overling” is the actor that called spawn. The new actor is recorded in the overlings set
of underlings.
Certain functions are intrinsic to the actor and cannot be overridden. Theyre assigned to each new actor instance at spawn time:
Overling & Underlings:
----------------------
• Overling: The actor that spawns another actor.
• Underling: An actor that is spawned by an overling.
1. **`spawn(script, config, callback)`**
Creates (spawns) a new actor from another script file.
- **`script`**: Path to the actor script (a file containing statements, not returning anything).
- **`config`**: Optional object of extra properties to assign to the new actor.
- **`callback(underling, info)`**: Optional function invoked right after the actor is instantiated but before it fully initializes.
Each actor has:
this.overling (read-only): The actor that spawned it.
this.underlings (read-only): A set of its child actors.
The newly spawned actor:
- Receives a reference to its parent (the `overling`) and can store child actors (the `underlings`).
- Automatically calls `awake()` if that function is defined, after basic setup completes.
- Registers any recognized event handlers (like `update`, `draw`, etc.) if they exist.
Actor Lifecycle & kill()
------------------------
An actor can be removed from the world by calling kill(). For example:
2. **`kill()`**
Destroys the actor, all of its timers, and recursively kills any underling (child) actors. If the actor has a parent, it is removed from the parents `underlings` set.
child.kill()
3. **`delay(fn, seconds)`**
Runs the given function `fn` after `seconds`. This is implemented under the hood with a timer that automatically clears itself once it fires.
- **Example**:
```js
this.delay(_ => {
console.log("3 seconds later!")
}, 3)
```
Killing an actor automatically terminates all of its underlings (recursively). Once killed, an actor is marked
as dead and will no longer process any calls. When the root actor (main.js) calls kill() on itself or is terminated,
the entire engine shuts down.
4. **`clear()`**
Recursively kills all child actors, clearing your immediate `underlings` set. This is not called automatically. You can use it to manually clean up all children without necessarily killing the actor itself.
Hooks and Functions:
--------------------
When an actor is spawned, the engine looks for special hook functions. These functions are optional, but if present,
they are called at specific times:
### The Actor Lifecycle
• awake() Called immediately after the actor is fully set up and registered with engine updates.
• garbage() Called just before the actor is removed; use this to free resources or unhook active listeners.
• then() Also called just before removal; can be used for additional cleanup or business logic.
1. **Creation**: An actor is spawned by calling `spawn(script, config)`. The script is loaded (unless already cached), then runs its program code.
2. **Awake**: If the new actor defines `awake()`, Prosperon calls it after the script finishes its top-level execution. This is a common place to do initialization.
3. **Registration**: If the actor has functions named `update`, `draw`, `physupdate`, etc., Prosperon automatically registers them. These can be used for per-frame logic, physics updates, or rendering.
4. **Garbage**: When the actor is killed, if it has a `garbage()` function, Prosperon calls it before final removal (useful for cleanup).
5. **Then**: If the actor has a `then()` function, Prosperon calls it at the very end of the kill process, allowing any final statements after your `garbage()` logic completes.
Example (child.js):
-------------------
// child.js
this.awake = function() {
console.log("Awake! My overling is " + this.overling);
}
this.garbage = function() {
console.log("Time to go away.");
}
### Overlings and Underlings
Delayed Calls:
--------------
To schedule a function to be called in the future on the actor, use:
this.delay(fn, seconds)
For example:
- **`this.overling`** is the parent actor that spawned the current one.
- **`this.underlings`** is a set of child actors that the current actor has spawned.
- Killing a parent automatically kills all of its underlings, which in turn can kill their own underlings, and so on.
this.delay(function() {
console.log("Called after 2 seconds");
}, 2)
### Event Registration (REGGIES)
The function will be called with “this” bound to the actor. If the actor is killed before the delay expires,
the scheduled function will not be executed.
Inside `engine.js`, theres a “registry” system for events like `update`, `physupdate`, `draw`, etc. An actor with a function named one of these triggers automatically registers that function. Prosperon calls all registered functions in order, every frame or whenever the corresponding event fires.
Actor Registration & Events:
----------------------------
Actors can automatically register to receive engine-wide events such as update, gui, etc. If an actor defines
a function with one of these names, it is registered automatically when the actor is created.
Because this happens automatically:
- Simply declare `this.update = function() { ... }` in your actor script, and Prosperon will call that every frame without manual event hookup.
- If you want to remove or replace an event handler, you can reassign or delete the function—though once the actor is created, its best to keep the same name.
Some common event hooks include:
update(): Called once per frame.
gui(): Called when GUI elements are drawn.
draw(): Called when the scene is rendered.
(Additional events such as appupdate or physupdate may also be supported based on engine configuration.)
### Actor Quirks
Example Actor (child.js):
-------------------------
// child.js
this.awake = function() {
console.log("Child actor awake.");
}
this.update = function() {
// Called every frame
console.log("Child actor update.");
}
this.garbage = function() {
console.log("Child actor is being removed.");
}
1. **Cannot Return a Value**
By definition, an actor script has no final `return` statement. If a script does have a return, its treated as a **module** instead.
2. **Cached Program**
Although an actor script is not a module, Prosperon still parses and caches it. Spawning an actor multiple times from the same script reuses the same parsed code, but each spawn yields a fresh actor instance.
3. **No Overriding Intrinsic**
Actor methods like `spawn`, `kill`, or `delay` cant be replaced. If you attempt to reassign them, Prosperon prevents it.
4. **Immediate Kill Restriction**
You cant kill an actor inside its own spawn-time code. Doing so throws an error. If you need to kill yourself immediately, use a small `delay` of zero, or let your `awake()` method handle it safely.
When another actor (including main.js) calls spawn("child.js"), the above code executes on the new actor.
## Program Documentation
main.js and the Engine Entry Point
------------------------------------
• Prosperon begins by loading and running main.js as the root actor.
• When main.js terminates or calls kill(), the entire engine shuts down.
Prosperon includes a module called `doc.js` which helps generate documentation for your modules and actors. This is done by scanning:
1. **Top-level docstring**: If an object has a property `object[prosperon.DOC]` or `object.doc`, doc.js treats it as descriptive text.
2. **Function docstrings**: If a function has `functionName.doc = "some text"`, doc.js records that text as the functions documentation.
3. **Sphinx-like directives**: doc.js looks for lines like `:param name: description` or `:return: something` inside docstrings. It transforms them into more readable bullet points in the final output.
Example (main.js):
------------------
// main.js
console.log("Hello from main.js");
this.awake = function() {
// Called once when the engine starts up
var child = this.spawn("child.js", { name: "ChildOne" });
console.log("Spawned a child actor.");
}
this.update = function() {
// Called every frame
// …
}
this.stopGame = function() {
// Call this function to exit the game
this.kill();
}
It is highly recommended to add documentation to your game's modules and actors, so prosperon can generate documentation for your project to keep everybody on the same page.
Logging and Console
-------------------
Prosperon provides several console logging methods:
### How to Add Docs
• console.log(msg) Info-level output (printed to both the log and console).
• console.debug(msg) Debug-level output.
• console.warn(msg) Warning-level messages.
• console.error(err) Error-level messages, including stack traces.
```js
// Suppose we have a module that returns a function
function greet(name) { console.log("Hello, " + name) }
A fatal error will print a stack trace and trigger an engine shutdown.
// We can attach a docstring
greet.doc = `
Greets the user by name.
:param name: The name of the person to greet.
`
Signals & Engine Shutdown
-------------------------
Internally, Prosperon listens for signals such as SIGINT, SIGABRT, SIGSEGV, etc. When these signals occur,
the engine will exit immediately. This behavior is a safety measure; scripting code typically does not need to handle
these signals directly.
// A single function is a valid return!
return greet
```
Summary of Key API
------------------
this.spawn(script, config, callback)
• Creates a new actor from the specified program script.
• Optionally merges properties from the config object into the new actor.
• Invokes the callback (if provided) with (actor, { message: "created" }).
this.kill()
• Immediately terminates the actor and all its underlings.
• Calls any garbage/then hooks defined on the actor.
this.delay(fn, seconds)
• Schedules the function fn to be called after the specified number of seconds, provided the actor remains alive.
Lifecycle Hooks:
• awake() Called immediately after an actor is spawned.
• garbage() Called just before the actor is removed.
• then() An additional cleanup callback before removal.
Event Hooks (automatically registered if defined):
• update() Called once per frame.
• gui(), draw(), appupdate(), etc.
Modules:
• Use modules by calling use('filename'). A module must return a single object from the module portion (i.e. the code
before the optional '---' if the file also contains a program portion).
When doc.js runs, it picks up this text and generates a Markdown file containing details like the function signature and your docstring. This is especially helpful for large projects or for building a reference of all available modules and methods.

View File

@@ -199,3 +199,7 @@ subdirectories.
**Returns**: An array of file (and directory) paths found.
## mount_core()
## is_directory()

1
docs/api/miniz.md Normal file
View File

@@ -0,0 +1 @@
# miniz

View File

@@ -1,4 +1,4 @@
# Dull
# The Dull Programming Language: Extensions to Javascript
The language is dubbed "dull". Dull, because it is a less featureful Javascript. Where does it differ?

View File

@@ -1,4 +1,5 @@
# Exporting your game
# Building & Releasing
Prosperon is a multiplatform engine. Bundling your game for these platforms essentially involves three steps:
- Baking static content

View File

@@ -1,20 +1,29 @@
# Getting Started
# Preface: The Prosperon Manifesto
Prosperon is based around a Javascript-like language, named "dull", engineered to be the quickest way to make computer games.
## Installation
## The Vision
The less lines of code it takes to do something, the less your program does. The less your program does, the faster it is, and the fewer bugs it has.
## A quick example
Prosperon is designed to achieve a minimal number of lines of code when creating a game. How does it do so?
## API usage
A lot of the API usage is informed from 'duck typing'. If something "looks" like a camera - it can be used like one! If something "looks" like a sprite, it can be used like one! There are fast paths on nearly everything for well defined objects
1. **Duck typing**
A lot of the API usage is informed from 'duck typing'. If something "looks" like a camera - it can be used like one! If something "looks" like a sprite, it can be used like one! This means that if you create an enemy in the game, you may be able to pass it in to render, while in other more strict languages, you might need to create an intermediate "sprite" object.
2. **Gradual performance**
However, there are fast paths on nearly everything for well defined objects. For most use cases, the flexible, duck typing works. But in specific circumstances, where speed is required, it's possible.
3. **Uniformity**
Uniformity is prioritized. Javascript allows for a powerful abstraction - the object - which Prosperon makes much use of. It allows for another - the arraybuffer - which makes it simple to plug different parts of the engine into each other.
The object is the lingua franca of the API. For example, json.encode and json.decode converts objects to and fromt json strings; nota.encode and nota.decode converts objects to and from nota bytes, represented as a javascript arraybuffer. To convert a json string to a nota buffer, one would do:
nota.encode(json.decode(str))
Most functions take objects just like this, increasing flexibility for what you can send into functions.
!!! scholium
The object is the lingua franca of the API. For example, json.encode and json.decode converts objects to and from json strings; nota.encode and nota.decode converts objects to and from nota bytes, represented as a javascript arraybuffer. To convert a json string to a nota buffer, one would do:
```nota.encode(json.decode(str))```
# Gradual performance
Prosperon makes it easy to make something quickly, and if it runs well, you're golden! But if it's slow, there are a plethora of options to make it faster. Plus, with native C plugins, there is nothing that can't be figured out.
4. **AI**
Prosperon is packed with tools to make it easy to work with AI. AI is an incredible productivity boost for programming, but it has a difficult time dealing with game engines like Unity and Unreal. Prosperon can, for example, generate an overview of your game, which you can plug into an AI for context so it will have an easy time helping with what you're stuck on.
## Installation
Just grab the prosperon build for your platform, drop it in a folder, and run it. Prosperon is a tiny executable - usually less than 2MB - so it's recommended to version it with your project.

View File

@@ -1,4 +1,5 @@
# Input
# Program events and an example input system
Input is done in a highly generic and customizable manner. *players* can take control of any object (actor or otherwise) in Prosperon, after which it is referred to as a *pawn* of a player. If the object has a defined *input* object, it is a valid pawn. One player can have many pawns, but each pawn may have only one player.
Pawns are added as a stack, with the newest ones getting priority, and handled first. It is possible for pawns to block input to lower pawns on the stack.

0
docs/prosperon.md Normal file
View File

View File

@@ -1,4 +1,4 @@
# Rendering System
# Rendering, the camera & the world
- **Pipeline & Shader Setup:**
The engine defines a base rendering pipeline and derives specialized pipelines (e.g. for sprites) by modifying properties such as blend state, stencil settings, and target textures.

View File

@@ -1,4 +1,4 @@
# Resource & I/O System Overview
# Resources & Data
This document provides a **high-level** explanation of how Prosperon handles resource loading, module discovery, and general file I/O. It is intended to give you insight into the **concepts and architecture** behind these systems, without detailing individual function calls.

62
docs/tutorial.md Normal file
View File

@@ -0,0 +1,62 @@
# A Tutorial Introduction
Let's make your first game. When prosperon is launched, it looks in the folder it's in for a ```config.js``` and ```main.js```. These are your first two examples of a module and a program.
A module is a file that returns a single value. It could be any value: a number, a function, a string. Usually, it's an object. Values are **frozen** before returning, so they cannot be modified. A module is ran once and cached, so any program that accesses a module subsequently is simply getting the already cached object. Modules can import other modules. Circular references are not allowed.
The ```config.js``` module must return a single object that describes your game. It sets up the window name, starting size, etc. Below is the default config, demonstrating all of the values that can be set on it.
| Key | Default Value | Type | Description |
|--------------------|----------------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------|
| **title** | `Prosperon [\${prosperon.version}-\${prosperon.revision}]` | string | Title of the game window, typically including version information. |
| **width** | `1280` | number | Initial width of the game window. |
| **height** | `720` | number | Initial height of the game window. |
| **icon** | `graphics.make_texture(io.slurpbytes('icons/moon.gif'))` | object | Icon texture for the game window, loaded from the provided file. |
| **high_dpi** | `0` | number | Enables (1) or disables (0) High DPI support for the window. |
| **alpha** | `1` | number | Alpha channel setting for the window (0 to disable, 1 to enable transparency). |
| **fullscreen** | `0` | number | Sets whether the window should launch in fullscreen (1) or windowed (0). |
| **sample_count** | `1` | number | Multisampling count for rendering. Increasing this can improve image quality at the cost of performance. |
| **enable_clipboard** | `true` | boolean | Enables clipboard functionality within the application. |
| **enable_dragndrop** | `true` | boolean | Enables drag-and-drop functionality for files into the window. |
| **max_dropped_files** | `1` | number | Maximum number of files that can be dropped into the window at once. |
| **swap_interval** | `1` | number | Controls vertical synchronization (VSync). Commonly 1 for enabling or 0 for disabling VSync. |
| **name** | `"Prosperon"` | string | Human-readable name of the application. |
| **version** | `prosperon.version + "-" + prosperon.revision` | string | Version string for the application, dynamically built from Prosperon's version info. |
| **identifier** | `"world.pockle.prosperon"` | string | Package or bundle identifier for your game/application. |
| **creator** | `"Pockle World LLC"` | string | Name of the creator or organization behind the application. |
| **copyright** | `"Copyright Pockle World 2025"` | string | Copyright declaration for the application. |
| **type** | `"application"` | string | The general content type or category of this project. |
| **url** | `"https://github.com/johnbrethauer/prosperon"` | string | URL link associated with the project, such as a repository or official homepage. |
With the engine configured, prosperon starts ```main.js``` as the first **actor** of the game. Actors are created from files that **do not** return a value. An actor executes the statements in its script. Initialization should happen here. An actor can pull in other chunks of code by importing modules. Modules are imported with the ```use``` statement. ```use``` returns the value the module returned, and it can be assigned to any variable. To use the internal ```io``` module, for example, you might say ```var io = use('io')```.
Actors exist until they are explicitly killed, by invoking their ```kill``` function. Because the actor created from ```main.js``` is the root actor for the entire game, when it is killed, the program exits. In an actor script file, ```this``` is set to the actor being spawned from the script.
## Our first program
With that out of the way, we can establish our first simple program.
In the folder with your ```prosperon``` executable, create a ```config.js``` and set it to be the following:
```
return {
title: "Hello World",
width: 500,
height:500
}
```
This will cause prosperon to launch a 500x500 window with the title 'Hello World'. In your ```main.js```, write the following:
```
console.log("Hello world")
this.delay(_ => {
this.kill();
}, 3)
```
**delay** is a function on all actors in prosperon. It executes a given function after a number of seconds. In this case, after 3 seconds, the root actor kills itself, closing the window.
## The prosperon global
There is a single global object defined for you, game wide, called `prosperon`. prosperon has a variety of engine specific settings on it that can be set to influence how the engine behaves. For example, `prosperon.argv` contains a list of the command line arguments given to prosperon; `prosperon.PATH` is an array of paths to resolve resources such as modules and images.

View File

@@ -90,7 +90,7 @@ endif
deps += dependency('qjs-layout',static:true)
deps += dependency('qjs-nota',static:true)
deps += dependency('qjs-miniz')
deps += dependency('qjs-miniz',static:true)
deps += dependency('qjs-soloud',static:true)
deps += dependency('physfs', static:true)

View File

@@ -83,25 +83,11 @@ prosperon.PATH = [
"scripts/modules/ext/",
]
function find_in_path(filename, exts = []) {
for (var dir of prosperon.PATH) {
var candidate = dir + filename;
if (io.exists(candidate)) return candidate;
for (var ext of exts) {
candidate = dir + filename + ext;
if (io.exists(candidate)) return candidate;
}
}
return undefined;
}
// path is the path of a module or script to resolve
var script_fn = function script_fn(path) {
var parsed = {}
var file = resources.find_script(find_in_path(path, ['.js', '.jsc']));
var file = resources.find_script(path);
if (!file) {
// attempt to bare load
@@ -292,7 +278,6 @@ switch(os.platform()) {
var use = function use(file) {
if (use_cache[file]) return use_cache[file];
var mod = script_fn(file)

View File

@@ -1,4 +1,3 @@
// doc.js
var ret = {}
var io = use('io')

16
scripts/modules/miniz.js Normal file
View File

@@ -0,0 +1,16 @@
var miniz = this
miniz.read[prosperon.DOC] = `Create a zip reader from the given ArrayBuffer containing an entire ZIP archive.
Return undefined if the data is invalid.
:param data: An ArrayBuffer with the entire ZIP file.
:return: A 'zip reader' object with methods for reading from the archive (mod, exists, slurp).
`
miniz.write[prosperon.DOC] = `Create a zip writer that writes to the specified file path. Overwrites the file if
it already exists. Return undefined on error.
:param path: The file path where the ZIP archive will be written.
:return: A 'zip writer' object with methods for adding files to the archive (add_file).
`
return miniz

View File

@@ -1,59 +1,203 @@
var io = use('io')
var miniz = use('miniz')
var os = use('os')
// Merge of the old resources.js and packer.js functionalities
var Resources = {}
var so_ext;
// Determine the shared library extension based on the OS
var so_ext
switch(os.platform()) {
case 'Windows':
so_ext = '.dll';
break;
so_ext = '.dll'
break
default:
so_ext = '.so';
break;
so_ext = '.so'
break
}
Resources.scripts = ["jsoc", "jsc", "jso", "js"]
// Recognized resource extensions
Resources.scripts = ["js"]
Resources.images = ["qoi", "png", "gif", "jpg", "jpeg", "ase", "aseprite"]
Resources.sounds = ["wav", "flac", "mp3", "qoa"]
Resources.fonts = ["ttf"]
Resources.lib = [so_ext]
Resources.canonical = function(file)
{
return io.realdir(file) + file
// Helper function: get extension from path in lowercase (e.g., "image.png" -> "png")
function getExtension(path) {
var idx = path.lastIndexOf('.')
if (idx < 0) return ''
return path.substring(idx + 1).toLowerCase()
}
// ext is a list of extensions to search
// returns the file to load
function find_ext(file, ext) {
if (!file) return;
// Return true if ext is in at least one of the recognized lists
function isRecognizedExtension(ext) {
if (!ext) return false
if (Resources.scripts.includes(ext)) return true
if (Resources.images.includes(ext)) return true
if (Resources.sounds.includes(ext)) return true
if (Resources.fonts.includes(ext)) return true
if (Resources.lib.includes('.' + ext)) return true // for .so or .dll
return false
}
var file_ext = file.ext()
if (file_ext && ext.some(e => e === file_ext))
if (io.exists(file)) return file
for (var e of ext) {
var attempt = `${file}.${e}`;
if (io.exists(attempt)) return attempt
// Attempt to find file with or without extension from the current PATH
// (From the original resources.js, unchanged except for code style)
function find_in_path(filename, exts = []) {
if (filename.includes('.')) {
for (var dir of prosperon.PATH) {
var candidate = dir + filename
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
}
return undefined
}
console.log(`Unable to find a file ${file} from extensions ${ext}`)
for (var dir of prosperon.PATH) {
var candidate = dir + filename
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
for (var ext of exts) {
candidate = dir + filename + '.' + ext
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
}
}
return undefined
}
Resources.find_image = function (file) {
return find_ext(file, Resources.images);
}.hashify();
// Return a canonical path (the real directory plus the path)
Resources.canonical = function(file) {
return io.realdir(file) + file
}
Resources.find_sound = function (file) {
return find_ext(file, Resources.sounds);
}.hashify();
// The resource finders
Resources.find_image = function(file) {
return find_in_path(file, Resources.images)
}.hashify()
Resources.find_script = function (file) {
return find_ext(file, Resources.scripts);
}.hashify();
Resources.find_sound = function(file) {
return find_in_path(file, Resources.sounds)
}.hashify()
Resources.find_script = function(file) {
return find_in_path(file, Resources.scripts)
}.hashify()
Resources.find_font = function(file) {
return find_ext(file, Resources.fonts);
}.hashify();
return find_in_path(file, Resources.fonts)
}.hashify()
// .prosperonignore reading helper
function read_ignore(dir) {
var path = dir + '/.prosperonignore'
var patterns = []
if (io.exists(path)) {
var lines = io.slurp(path).split('\n')
for (var line of lines) {
line = line.trim()
if (!line || line.startsWith('#')) continue
patterns.push(line)
}
}
return patterns
}
// Return a list of recognized files in the directory (and subdirectories),
// skipping those matched by .prosperonignore. Directory paths are skipped.
Resources.getAllFiles = function(dir = "") {
var patterns = read_ignore(dir)
var all = io.globfs(patterns, dir)
var results = []
for (var f of all) {
var fullPath = dir + '/' + f
try {
var st = io.stat(fullPath)
// skip directories (filesize=0) or unrecognized extension
if (!st.filesize) continue
var ext = getExtension(f)
if (!isRecognizedExtension(ext)) continue
results.push(fullPath)
} catch(e) {}
}
return results
}
Resources.getAllFiles[prosperon.DOC] = `
Return a list of recognized files in the given directory that are not matched by
.prosperonignore, skipping directories. Recognized extensions include scripts,
images, sounds, fonts, and libs.
:param dir: The directory to search.
:return: An array of recognized file paths.
`
// Categorize files by resource type
Resources.gatherStats = function(filePaths) {
var stats = {
scripts:0, images:0, sounds:0, fonts:0, lib:0, other:0, total:filePaths.length
}
for (var path of filePaths) {
var ext = getExtension(path)
if (Resources.scripts.includes(ext)) {
stats.scripts++
continue
}
if (Resources.images.includes(ext)) {
stats.images++
continue
}
if (Resources.sounds.includes(ext)) {
stats.sounds++
continue
}
if (Resources.fonts.includes(ext)) {
stats.fonts++
continue
}
// For so_ext, we store it in Resources.lib as ['.so'] or ['.dll'], so match that form
if (Resources.lib.includes('.' + ext)) {
stats.lib++
continue
}
stats.other++
}
return stats
}
Resources.gatherStats[prosperon.DOC] = `
Analyze a list of recognized files and categorize them by scripts, images, sounds,
fonts, libs, or other. Return a stats object with these counts and the total.
:param filePaths: An array of file paths to analyze.
:return: { scripts, images, sounds, fonts, lib, other, total }
`
// Create a ZIP of recognized files in a directory, skipping ignored ones, and write to outPath
Resources.pack = function(dir, outPath) {
if (!io.exists(dir))
throw Error("Directory does not exist: " + dir)
var files = Resources.getAllFiles(dir)
var writer = miniz.write(outPath)
for (var fullPath of files) {
try {
var st = io.stat(fullPath)
if (!st.filesize) continue // skip directories
// Path in the ZIP: remove leading dir + '/'
var pathInZip = fullPath.substring(dir.length + 1)
var data = io.slurpbytes(fullPath)
writer.add_file(pathInZip, data)
} catch(e) {
// Optionally handle or log errors
}
}
}
Resources.pack[prosperon.DOC] = `
Create a ZIP archive of all recognized files (skipping those matched by .prosperonignore)
in the specified directory and write it to outPath. Recognized extensions are scripts,
images, sounds, fonts, or libs.
:param dir: The directory to zip.
:param outPath: The path (including filename) for the resulting ZIP file.
:return: None
:raises Error: If the directory does not exist.
`
return Resources

View File

@@ -6123,6 +6123,25 @@ JSC_CCALL(io_searchpath,
JS_SetPropertyUint32(js,ret,i,JS_NewString(js,paths[i]));
)
extern void prosperon_mount_core();
JSC_CCALL(io_mount_core,
int mount = JS_ToBool(js,argv[0]);
if (!mount)
PHYSFS_unmount("core.zip");
else
prosperon_mount_core();
)
JSC_SCALL(io_is_directory,
PHYSFS_Stat stat;
int good = PHYSFS_stat(str, &stat);
if (!good)
ret = JS_NewBool(js, 0);
else
ret = JS_NewBool(js, stat.filetype == PHYSFS_FILETYPE_DIRECTORY);
)
static const JSCFunctionListEntry js_io_funcs[] = {
MIST_FUNC_DEF(io, rm, 1),
MIST_FUNC_DEF(io, mkdir, 1),
@@ -6142,6 +6161,8 @@ static const JSCFunctionListEntry js_io_funcs[] = {
MIST_FUNC_DEF(io, open, 1),
MIST_FUNC_DEF(io, searchpath, 0),
MIST_FUNC_DEF(io, enumerate, 2),
MIST_FUNC_DEF(io, mount_core, 1),
MIST_FUNC_DEF(io, is_directory, 1),
};
JSC_CCALL(file_close,
@@ -7598,6 +7619,7 @@ JS_SetPrototype(js, js_##NAME, PARENT); \
JSValue js_layout_use(JSContext *js);
JSValue js_soloud_use(JSContext *js);
JSValue js_miniz_use(JSContext *js);
#ifdef TRACY_ENABLE
JSValue js_tracy_use(JSContext *js);
@@ -7668,6 +7690,7 @@ void ffi_load(JSContext *js, int argc, char **argv) {
arrput(module_registry, MISTLINE(event));
arrput(module_registry, MISTLINE(soloud));
arrput(module_registry,MISTLINE(layout));
arrput(module_registry, MISTLINE(miniz));
#ifndef NEDITOR
arrput(module_registry, MISTLINE(imgui));
#endif

View File

@@ -6,30 +6,32 @@
#include <stdint.h>
static unsigned char *zip_buffer_global;
static char *prosperon;
void free_zip(void)
{
free(zip_buffer_global);
zip_buffer_global = NULL;
}
int main(int argc, char **argv) {
printf("%s\n", argv[0]);
FILE *f = fopen(argv[0], "rb");
if (!f) { perror("fopen"); return 1; }
if (fseek(f, 0, SEEK_END) != 0) { perror("fseek"); fclose(f); return 1; }
long size = ftell(f);
if (size < 0) { perror("ftell"); fclose(f); return 1; }
zip_buffer_global = malloc(size);
if (!zip_buffer_global) { perror("malloc"); fclose(f); return 1; }
rewind(f);
if (fread(zip_buffer_global, 1, size, f) != (size_t)size) {
perror("fread");
free(zip_buffer_global);
fclose(f);
return 1;
}
void prosperon_mount_core()
{
size_t size;
FILE *f = fopen(prosperon, "rb");
if (!f) { perror("fopen"); return; }
if (fseek(f, 0, SEEK_END) != 0) { perror("fseek"); fclose(f); return; }
size = ftell(f);
if (size < 0) { perror("ftell"); fclose(f); return; }
zip_buffer_global = malloc(size);
if (!zip_buffer_global) { perror("malloc"); fclose(f); return; }
rewind(f);
if (fread(zip_buffer_global, 1, size, f) != (size_t)size) {
perror("fread");
free(zip_buffer_global);
fclose(f);
return;
}
fclose(f);
// Search backwards for the EOCD signature "PK\x05\x06".
// The EOCD record is at most 0xFFFF (65535) bytes plus 22 bytes long.
long max_comment_len = 0xFFFF;
@@ -47,9 +49,9 @@ int main(int argc, char **argv) {
if (eocd_pos < 0) {
fprintf(stderr, "EOCD not found\n");
free(zip_buffer_global);
return 1;
return;
}
// Parse the EOCD record.
// EOCD record layout (without the comment):
// Offset 0: 4 bytes signature ("PK\x05\x06")
@@ -61,7 +63,7 @@ int main(int argc, char **argv) {
// Offset 16:4 bytes offset of start of central directory (cd_offset_rel, relative to zip start)
// Offset 20:2 bytes comment length
uint16_t comment_length = zip_buffer_global[eocd_pos + 20] |
(zip_buffer_global[eocd_pos + 21] << 8);
(zip_buffer_global[eocd_pos + 21] << 8);
int eocd_size = 22 + comment_length;
uint32_t cd_size = zip_buffer_global[eocd_pos + 12] |
(zip_buffer_global[eocd_pos + 13] << 8) |
@@ -71,32 +73,34 @@ int main(int argc, char **argv) {
(zip_buffer_global[eocd_pos + 17] << 8) |
(zip_buffer_global[eocd_pos + 18] << 16) |
(zip_buffer_global[eocd_pos + 19] << 24);
// The size of the appended zip archive is given by:
// appended_zip_size = (offset of central directory + size of central directory + EOCD record size)
// Since the EOCD record is the last part of the zip archive,
// we can compute the start of the zip archive (zip_offset) as:
uint32_t appended_zip_size = cd_offset_rel + cd_size + eocd_size;
long zip_offset = size - appended_zip_size;
if (zip_offset < 0 || zip_offset >= size) {
fprintf(stderr, "Invalid zip offset computed: %ld\n", zip_offset);
free(zip_buffer_global);
return 1;
return;
}
printf("Zip data found at offset %ld, appended zip size %u bytes\n",
zip_offset, appended_zip_size);
int ret = PHYSFS_mountMemory(zip_buffer_global + zip_offset, appended_zip_size, free_zip, "core.zip", NULL, 0);
if (!ret) {
printf("COULD NOT MOUNT! Reason: %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
return;
}
}
int main(int argc, char **argv) {
prosperon = argv[0];
PHYSFS_init(argv[0]);
char *base = PHYSFS_getBaseDir();
PHYSFS_setWriteDir(base);
PHYSFS_mount(base, "/", 0);
int ret = PHYSFS_mountMemory(zip_buffer_global + zip_offset, appended_zip_size, free_zip, "core.zip", NULL, 0);
prosperon_mount_core();
if (!ret) {
printf("COULD NOT MOUNT! Reason: %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
return 1;
}
script_startup(argc, argv); // runs engine.js
return 0;