Skip to content
 
 

Repository files navigation

Pip-Boy 3000 Holotapes

A community driven repository of custom applications and games for the Pip-Boy 3000, hosted on pip-boy.com.

Pip-Boy.com  |  Discord Community  |  Bethesda Store  |  The Wand Company  |  Espruino  |  RobCo Industries

Index

Description

Pip-Boy 3000 Holotapes by the community, for the community.

Install on: pip-boy.com

Follow the guide below to create your own custom Holotapes for the Pip-Boy 3000!

[ Index ]

Creating a new Holotape

  1. Create a directory for the app or game under holotapes/. Every Holotape must use this structure:

    holotapes/<YourHolotape>/
    ├── app.js
    ├── app.min.js
    ├── metadata.json
    ├── README.md
    ├── ChangeLog
    └── assets/
    
  2. Write the unminified source in app.js. The app must be an anonymous function expression that the Pip-Boy OS invokes; do not invoke it yourself with a trailing (). It must return an uppercase alphanumeric id and a remove function.

    Example app:

    Expand/Collapse
    (function () {
      const W = h.getWidth(),
        H = h.getHeight();
      let leftWheel = 0,
        rightWheel = 0,
        lastInput = 'NONE';
    
      function draw() {
        h.clear(0);
        h.setColor(3)
          .setFontMonofonto28()
          .setFontAlign(0, 0)
          .drawString('EXAMPLE', W / 2, 50)
          .setFontMonofonto18()
          .drawString('LAST: ' + lastInput, W / 2, 100)
          .setFontMonofonto16()
          .setFontAlign(-1, -1)
          .drawString('LEFT WHEEL: ' + leftWheel, 80, 145)
          .drawString('RIGHT WHEEL: ' + rightWheel, 80, 175)
          .drawString('PRESS THE LEFT WHEEL TO RESET', 80, H - 40);
      }
    
      function onKnob1(dir, long) {
        if (dir) {
          leftWheel += dir;
          lastInput = dir < 0 ? 'LEFT UP' : 'LEFT DOWN';
          Pip.playSound('SCROLL');
        } else {
          leftWheel = 0;
          lastInput = long ? 'LEFT LONG PRESS' : 'LEFT PRESS';
          Pip.playSound('TAB');
        }
        draw();
      }
    
      function onKnob2(dir) {
        if (dir) {
          rightWheel += dir;
          lastInput = dir < 0 ? 'RIGHT UP' : 'RIGHT DOWN';
          Pip.playSound('SCROLL');
        }
        draw();
      }
    
      Pip.audioStop();
      Pip.onExclusive('knob1', onKnob1);
      Pip.onExclusive('knob2', onKnob2);
      draw();
    
      return {
        id: 'EXAMPLE',
        notDefault: true,
        fullscreen: true,
        remove: function () {
          Pip.removeListener('knob1', onKnob1);
          Pip.removeListener('knob2', onKnob2);
          Pip.audioStop();
          h.clear();
        },
      };
    });
  3. Add metadata.json. Asset paths and storage URLs are relative to the Holotape directory. The storage directory is the metadata id converted to uppercase, with underscores replacing hyphens.

    {
      "id": "example",
      "name": "Example Holotape",
      "author": "@your-github-username",
      "version": "1.0.0",
      "description": "A short, one-sentence description.",
      "icon": "assets/icon.png",
      "previews": [],
      "type": "app",
      "readme": "README.md",
      "storage": [{ "name": "HOLO/EXAMPLE/APP.JS", "url": "app.min.js" }],
      "storageOptional": []
    }

    The metadata id must contain only lowercase letters, numbers, and hyphens; version must use semantic versioning; and type must be exactly app or game. Do not edit holotapes/registry.json manually. npm run build generates it from each metadata.json file.

  4. Document the description, controls, installation, tested firmware, and credits in the Holotape's README.md.

  5. Add at least one ChangeLog entry in this format:

    1.0.0 (yyyy-mm-dd)
    <pull-request-or-change-link>
    - Initial release
    
  6. Generate app.min.js from app.js as described in Build and minification. Both files must behave identically.

[ Index ]

Development Workflow

Using Pip-Boy.com

Expand/Collapse
  1. Open the Pip-Boy 3000 Holotape Creator/Editor.

  2. Create a new Holotape and give it a name.

  3. Create or edit the Holotape code in the built-in editor.

  4. Test your Holotape on the device using the "Save & Test" button.

    img-info The editor's "Encode" button can prepare code for testing. Keep the readable source as app.js and the encoded/minified output as app.min.js.

  5. Download your files and add them to this repository.

Using the Espruino Web IDE

You can use one of the two methods below to upload and test your Holotape:

Expand/Collapse
  1. Open the Espruino Web IDE or its GitHub-hosted version.

  2. Open your file:

    img-open-file

  3. Enable Watch File.

    img-watch-file

  4. Edit the app in VS Code or the Web IDE's built-in editor.

  5. Enable Settings > Minification > Esprima: Mangle.

  6. Set Settings > Minification > Pretokenise code before upload to Yes/Always.

  7. Upload to the device for testing.

img-info You can use a boot code file to boot straight into the app.

Build and minification

Minify and Encode your Holotape here:

https://www.pip-boy.com/3000/holotapes/create

This will give you a proper app.min.js from app.js.

Run the repository build after adding or changing metadata:

npm install
npm run build

This rebuilds holotapes/registry.json, rewrites relative file paths for the registry, and rejects metadata whose type is not app or game. It does not generate app.min.js or perform all of the submission checks for you.

[ Index ]

Images

Expand/Collapse

Holotape images must be bitmaps with a maximum color depth of 4bpp. Convert source artwork with the Image Converter.

Prefer one h.drawImage() call over many procedural drawing calls for sprites.

Small sprites can be stored inline:

const sprites = { icon: atob('...') };
h.drawImage(sprites.icon, 120, 80);

Larger collections can be stored separately and loaded from the SD card. Defer large asset loads with setTimeout(..., 0) and call E.defrag() first:

let sprites,
  assetTimeout = setTimeout(function () {
    E.defrag();
    sprites = eval(require('fs').readFileSync('HOLO/MYAPP/IMG.JS'));
    h.drawImage(sprites.icon, 120, 80);
  }, 0);

Any asset timeout must be cleared in remove(). Very large backgrounds can be streamed into h.buffer with E.openFile() and Uint8Array instead of being held as another complete image in memory.

Metadata icons must be PNG files. Preview files may be PNG, GIF, or MP4, and their paths must be relative to the Holotape directory.

[ Index ]

Input handling

Expand/Collapse

Use Pip.onExclusive() when an app needs exclusive control input handling:

function onKnob1(dir, long) {
  if (dir === 1) {
    // Down / clockwise
  } else if (dir === -1) {
    // Up / counter-clockwise
  } else if (long) {
    // Long press
  } else {
    // Normal press
  }
}

Pip.onExclusive('knob1', onKnob1);

Pip.onExclusive() is the preferred default. Use Pip.on() only when the app intentionally needs to coexist with another handler. A setWatch() on ENC1_PRESS is reserved for unusually latency-sensitive press handling. A direct watch on a button such as BTN_DATA is appropriate only when no Pip.on() event exists.

Remove every listener and watch when the app exits:

Pip.removeListener('knob1', onKnob1);
clearWatch(buttonWatch);

For text entry, firmware 1.1.4 and later provides a built-in on-screen keyboard:

Pip.createKeyboard(initialText, description, callback);

The keyboard takes exclusive control of both knobs and the callback receives the current text when the user selects Enter. It does not close itself: the call returns an object with a remove() method, and you must call .remove() on it (typically inside the callback) before drawing your next screen. On older firmware there is no global keyboard API; see agents.md for a showTextEntry-style implementation you can copy if you need to support pre-1.1.4 devices.

[ Index ]

Memory and Performance

Expand/Collapse

Main rules:

  • The display is always 480×320. If dimensions are needed repeatedly, declare them once as const W = h.getWidth(), H = h.getHeight().

  • Every variable consumes a scarce Espruino block. Use const for constants, let for mutable state, never var, and inline single-use values.

  • Always use the global h graphics object directly and chain graphics calls. Do not spend a variable block on an alias for h.

  • Use h.clearRect(), clipping, cached wrapped text, and dirty flags to redraw only changed regions.

  • Timer-driven apps normally rely on the OS auto-flush. Do not call h.flip() from a setInterval() loop. For Pip.onFrame rendering, set Pip.lastFlip = getTime() before drawing and call h.flip() after drawing.

  • Use "ram" for performance-critical frame functions and "jit" for small, numeric tight loops. Do not use either without a measured need.

  • Use Math.randInt(n) instead of Math.random(), and use typed arrays for dense numeric data.

  • Espruino does not support async/await, ES modules, template literals, fetch(), XMLHttpRequest, or requestAnimationFrame().

  • Keep the app scoped:

    (function () {
      // App code here
      return {
        id: 'APPID',
        notDefault: true,
        fullscreen: true,
        remove: function () {
          /* clean up app resources */
        },
      };
    });
  • Clean up every resource created by the app in remove():

    remove: function() {
      Pip.removeListener("knob1", onKnob1);
      clearInterval(intervalId);
      clearTimeout(timeoutId);
      clearWatch(watchId);
      Pip.audioStop();
      h.clear();
    }
  • Never call load() or E.reboot() from remove(), call Pip.remove() at the top of the app, use a bare clearWatch(), or delete/reassign OS globals.

Useful memory checks:

process.memory();
print(E.getSizeOf(this, 1).sort((a, b) => a.size - b.size));
print(E.getSizeOf(Pip, 1).sort((a, b) => a.size - b.size));
print(E.getSizeOf(this['\xFF'], 1).sort((a, b) => a.size - b.size));

this['\xFF'] shows timers, watches, and internal runtime state.

Pip.CURRENT can hold the current page or app code.

[ Index ]

Contributing

Expand/Collapse
  1. Fork the repository:

    https://github.com/CodyTolene/pip-boy-3000-holotapes/fork

  2. Clone your fork and enter the repository:

    git clone https://github.com/<my-username>/pip-boy-3000-holotapes.git
    cd pip-boy-3000-holotapes

    Info Replace <my-username> with your GitHub username.

  3. Sync your fork before starting. On your fork's GitHub page, select Sync fork > Update branch. Then update local main and merge it into your working branch:

    git checkout main
    git pull origin main
    git checkout -b <my-branch>
    git merge main
    git push origin <my-branch>

    To merge all the new updates from the original repository's main branch directly into your checked out working branch:

    git checkout <my-branch>
    git pull https://github.com/CodyTolene/pip-boy-3000-holotapes.git main
    git push origin <my-branch>

    Replace <my-branch> with your branch name. If the branch does not exist yet, create it from the updated main branch with:

    git checkout -b my-holotape
  4. Make the change. New Holotapes must include all files described in Creating a new Holotape.

    Warn Submissions must include the original, human-readable source code (app.js). Pull requests containing only minified code (app.min.js) will be rejected. This is an open source project, and readable source is essential so the community can review changes, maintain and update apps over time, fix bugs, and learn from each other's work.

  5. Run the repository build and test the Holotape on the device:

    npm install
    npm run build
  6. Before opening a pull request, verify:

    • The original unminified source (app.js) is included; minified code alone is not accepted.
    • app.js starts with (function() {, ends with });, and is not invoked.
    • The return object contains a literal uppercase alphanumeric id and a remove function.
    • Every listener, interval, timeout, and watch is removed or cleared; audio is stopped if used.
    • remove() exits cleanly without load() or E.reboot().
    • No unsupported language/runtime features or OS-global mutations are used.
    • app.min.js exists and behaves exactly like app.js.
    • Metadata uses a unique lowercase ID, semantic version, valid type, valid relative paths, and the matching HOLO/<APP_ID>/ storage prefix.
    • README.md documents controls and ChangeLog contains an entry.
    • Images are converted bitmaps at 4bpp or less.
    • The app can be opened, closed, and opened again without leaking resources.
    • Memory usage is acceptable before, during, and after the app runs.
  7. Stage, commit, and push your changes. For a branch named <my-branch>:

    git add -A && git commit -m "My change description" && git push origin <my-branch>
  8. Open a pull request from your working branch to this repository's main branch. Describe the change and include screenshots, GIFs, or video previews when the UI changed.

[ Index ]

License

This repository is licensed under the MIT License.

All code, holotapes, apps, games, scripts, metadata, documentation, and other contributions submitted to this repository must be licensed under the MIT License unless explicitly stated otherwise by the repository maintainer in writing.

By submitting a pull request or contribution, you agree that your contribution is provided under the MIT License. See CONTRIBUTING.md for details.

See the LICENSE file for details.

SPDX-License-Identifier: MIT

[ Index ]

About

A repository for Pip-Boy 3000a apps & games created by the community, and hosted on Pip-Boy.com

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages