Skip to content

[p5.js 2.0 Beta Bug Report]: p5.registerAddon instance access in global mode (init lifecycle support) #7742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
2 of 17 tasks
quinton-ashley opened this issue Apr 16, 2025 · 37 comments

Comments

@quinton-ashley
Copy link
Contributor

Most appropriate sub-area of p5.js?

  • Accessibility
  • Color
  • Core/Environment/Rendering
  • Data
  • DOM
  • Events
  • Image
  • IO
  • Math
  • Typography
  • Utilities
  • WebGL
  • Build process
  • Unit testing
  • Internationalization
  • Friendly errors
  • Other (specify if possible)

p5.js version

2.0-beta8

Web browser and version

Chrome

Operating system

macOS

Steps to reproduce this

First I'd like to thank @limzykenneth for the extensive documentation on the new p5.registerAddon function.

https://dev.to/limzykenneth/designing-an-addon-library-system-for-p5js-20-3d4p

But this bit is only partially true.

"init" You no longer need a lifecycle hook for this, just put the relevant code in the addon function body or the presetup hook.

I understand that the standard way to add functions to p5 would be to add them to fn. But it's also important for addon developers to be able to add to each p5 instance, so that each instance can have its own data with its own state. For example in p5play, a separate physics simulation is created for each p5 instance.

The "init" hook in p5 v1 enabled access to the p5 instance but p5.registerAddon, since it's a static method, does not.

p5.registerAddon(function (p5, fn, lifecycles) {
	this.tester = 'hi'; // `this` is `window`
});

let p = new p5(() => {});
console.log(p.tester); // undefined

I got this to work when using p5 in instance mode but not in global mode.

p5.registerAddon(function (p5, fn, lifecycles) {
	lifecycles.presetup = function () {
		this.tester = 'hi'; // `this` is the instance
	};
});

let p = new p5(() => {});

p.setup = async function () {
	console.log(p.tester); // "hi"
};
p5.registerAddon(function (p5, fn, lifecycles) {
	lifecycles.presetup = function () {
		this.tester = 'hi';  // `this` is the instance
	};
});

async function setup() {
	console.log(tester); // undefined
}

To fix the issue I suggest adding back the ability to define "init" hooks that run before p5 adds stuff to the global scope in global mode.

@ksen0 ksen0 changed the title [p5.js 2.0 Beta Bug Report]: [p5.js 2.0 Beta Bug Report]: p5.registerAddon instance access in global mode (init lifecycle support) Apr 16, 2025
@davepagurek
Copy link
Contributor

Is the init hook more or less what presetup would already be? e.g., addon code:

function myAddon(p5, fn, lifecycles) {
  lifecycles.presetup = function() {
    const instance = this;
    instance.classObjects = [];

    p5.MyClass = class {
      constructor() {
        instance.classObjects.push(this);
      }
    }
  }

  fn.logClassObjects = function() {
    console.log(this.classObjects);
  }
}

user code:

let myObj1;
let myObj2;

function setup() {
  createCanvas(200, 200);
  myObj1 = new p5.MyClass();
  myObj2 = new p5.MyClass();
  logClassObjects(); // logs myObj1 and myObj2
}

The main thing I can see that would be a little finicky is that the preload compatibility addon also uses a presetup hook, so the order of addon imports would matter: in the above code, myAddon would have to be loaded before the preload addon for p5.MyClass to be available in preload.

@davepagurek
Copy link
Contributor

If we want to make sure there's a way to run before everything without worrying too much about preloads, I think just adding another init hook is probably the way to go (can't just bind this when registering the addon like we used to, as there isn't an instance to bind to at the time addons are set up.) So I'm ok with that too if that works for you @limzykenneth

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 16, 2025

Is the init hook more or less what presetup would already be?

No, you can run the examples I made to verify that the desired behavior can not be achieved with presetup currently.

The init (or presetup) hooks should ideally be run after all the included p5.js bundle addons are added but before instance props and functions are added to window in global mode.

Same as init in p5 v1.

@davepagurek
Copy link
Contributor

@limzykenneth if the intention is to use presetup for this use case, then we can move the global binding to after presetup is called, but otherwise adding a separate init hook before then also works.

@davepagurek
Copy link
Contributor

Just a status update: we'll discuss some more to iron out exactly what the API for this will be. It likely won't be in 2.0.0 because we're locking in the feature set to be able to do a release shortly, but we're planning on doing a follow-up release fairly soon after, so we'll still get to this!

@ksen0 ksen0 added this to the 2.x Anytime milestone Apr 17, 2025
@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

After looking more at this with @limzykenneth , here's an example that shows global state management:

p5.registerAddon(function (p5, fn, lifecycles) {

  lifecycles.presetup = function () {
    this.globalVar += "B";
  };

});

p5.registerAddon(function (p5, fn, lifecycles) {

  fn.globalVar = "A"; // fn is an alias for p5.prototype. This will be exported to global mode

  lifecycles.presetup = function () {
    this.globalVar += "C";
  };

});

async function setup() {
  console.log(this.globalVar); // This will be ABC
}

To better show the order of execution, I've included 2 hypothetical addons that both access 1 global var. So, first everything in "registerAddon" functions is run in order (setting "A"), then the presetup code is run in order (appending "B" and "C").

@quinton-ashley would this support your intended usage? If so we can convert this issue to a documentation task, eg on the contributor documentation for library creation or perhaps combining with some of the other global mode usages, since I'm sure state management in global mode is something others would really like to have a clearer overview of.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@ksen0 Hmm I guess that technically works (EDIT: I was wrong, it'd break preload support) but it's cumbersome and unintuitive compared to the old way of attaching to this in an "init" hook.

@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

@quinton-ashley ok let's keep this open for technical ideas, also please comment if the below example is not quite reflecting the intended use.

@davepagurek do you think there's a technically plausible (and maintainable) way to do the equivalent of fn.globalVar initialization before presetup when globalVar is bound to this in presetup but does not exist on the p5.prototype? To produce the following behavior?

p5.registerAddon(function (p5, fn, lifecycles) {

  //   fn.globalVar = ""; <-- could this be implicit? 

  lifecycles.presetup = function () {
    this.globalVar = "B";
  };

});

p5.registerAddon(function (p5, fn, lifecycles) {

  lifecycles.presetup = function () {
    this.globalVar = "C";
  };

});

async function setup() {
  console.log(this.globalVar); // This would be C in this hypothetical scenario
}

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@ksen0 you can't add to a string that doesn't exist.

There's no way for p5 to anticipate what addon devs are going to name their variables or what type of variables they should be.

@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

@ksen0 you can't add to a string that doesn't exist.

Removed append in the ex.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@ksen0 Okay yes that's the desired behavior.

There's no easier way to implement it though.

@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

I also don't see any way to do implicit global state management. Because the behavior is already possible (if a bit awkward), then I would argue it's best not to add another hook (also awkward in other places/ways). So either (1) someone has an idea about a technical solution that doesn't require init hook, or (2) there's other needs for init hook. I propose keeping this open for a while to see if there is any further input on either (1) or (2)!

@davepagurek
Copy link
Contributor

I think that would mean adding something like await this._runLifecycleHook('init'); right before this:

p5.js/src/core/main.js

Lines 136 to 148 in 55ed78e

for (const p of Object.getOwnPropertyNames(p5.prototype)) {
if(p[0] === '_') continue;
bindGlobal(p);
}
const protectedProperties = ['constructor', 'length'];
// Attach its properties to the window
for (const p in this) {
if (this.hasOwnProperty(p)) {
if(p[0] === '_' || protectedProperties.includes(p)) continue;
bindGlobal(p);
}
}

At this point I believe addons have been registered and the current instance exists so I think that it has everything it needs?

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@ksen0 The other reason to have a separate "init" hook is for it to run before preload.

Consider that the two biggest addon library developers, Dan Shiffman and myself, still want to offer preload system support when p5 v2 is used with ml5 and p5play respectively.

ml5js/ml5-next-gen#244 (comment)

Also I don't want to do state management in such an awkward way. You don't mean to suggest that the addon system in p5 v2 should offer less functionality than the addon system of p5 v1 right? It seems removing "init" was simply an oversight and the sooner we can fix that mistake the better.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@davepagurek Yup!

@ksen0 Should I make a PR for this?

@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

Thanks for this idea @davepagurek, I think it's a better solution than adding a lifecycle stage, yes @quinton-ashley please feel free to file the PR, thanks!

But please keep this issue open as ideally it'd be great to add a couple of tests to cover this behavior in instance and global mode to prevent future regression, and I don't think that's straightforward in vitest right now or necessary for the fix. Edited to remove this note, since testability of global mode is a task outside of this issue, and I've made a note of it in a global mode docs/tests improvements issue draft.

@davepagurek
Copy link
Contributor

A good start would be to upload a build with the new behaviour to e.g. the p5 web editor and just manually try registering a dummy addon that sets a property on the instance in preload, and use that property globally in a test sketch. We aren't quite sure what our current test suite's capacity to test global mode is, so something manual is good for now.

@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

A good start would be to upload a build with the new behaviour to e.g. the p5 web editor and just manually try registering a dummy addon that sets a property on the instance in preload, and use that property globally in a test sketch. We aren't quite sure what our current test suite's capacity to test global mode is, so something manual is good for now.

That's totally reasonable, I've edited my comment. I do still feel like global mode behaviors could be a bit better documented and tested, but you're right. It's challenging enough to automate right now that it doesn't need to stop this issue from being resolved. Thanks!

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@davepagurek Can't use await there because the constructor can't be async.

@ksen0 Making a separate "init" hook will ensure existing behavior of "presetup" hooks, which can be async functions. Will demo this in a PR.

@davepagurek
Copy link
Contributor

We can also move the global binding to be in here after presetup, which I think should be equivalent? We can add a similar check to this._isGlobal before doing so.

await this._runLifecycleHook('presetup');

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@davepagurek That'd make it impossible to implement top level global mode in the future.

It'd also break preload compatibility to have global binding be done there, since preload is called inside a presetup hook.

I was wrong, this issue can't be fixed by changing "presetup" and the workaround suggested by @ksen0 is not valid either, my bad. There needs to be a separate "init" hook.

@davepagurek
Copy link
Contributor

It's true we probably need globals initially bound before presetup for the preload addon to work (it registers a presetup hook and calls users' preload methods, which presumably call global methods like loadImage in it.) A possible solution there is to bind globals first as we currently do, and then after presetup, check if there are any new instance properties, and bind those if so.

@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

Ok thanks for looking into it, both. I think it would be very helpful for users to be able to declare global vars easily, but I think it is also important to recognize that, to the best of my understanding, the introduction of presetup (and the other lifecycle hooks) has been an intentional design choice. I am open to the concept of init only with some more careful consideration.

Can you explain what you mean by the workaround not being valid, which workaround, the fn.globalVar one? This still seems to work with 2.0 and preload addon: https://editor.p5js.org/ksen0/sketches/onGzvodWB - or did you mean something else?

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@ksen0 This is the output I get:

preload happens 
A 
setup happens 
ABC 

If the workaround solved the issue, then "ABC" would be printed from preload.

The issue is that p5 instance props need to be added to the global scope before preload runs, which also means before presetup hooks run.

I understand removing "init" was intentional but the intent was not to remove the functionality it offered, rather to replace it with equivalent functionality, which I've demonstrated and you've demonstrated is not the case.

@davepagurek That would prevent addons from overriding p5 functions. I do that in p5play to disable image loading/displaying as a debug feature. Also p5play's image caching and color palette system depend on it.

Sometimes the most obvious solution to a problem ("init" hooks in this case) is also the cleanest and simplest right? Just two lines of code to implement. 😅

@ksen0
Copy link
Member

ksen0 commented Apr 25, 2025

If the workaround solved the issue, then "ABC" would be printed from preload.

The workaround provides a way to have a global variable usable in global mode, so that part does work. I wasn't trying to get "ABC" printed in preload, but that's because I am not understanding something really important here I think, but I would really like to understand. If you need something in preload(), why can fn.varName = ... not be used? (Ie, if I need ABC in preload, then I could have set ABC in fn.globalVar=...)

Sometimes the most obvious solution to a problem ("init" hooks in this case) is also the cleanest and simplest right? Just two lines of code to implement.

Those 2 particular lines also introduce additional bugs (or at minimum questions), so while I'm not opposed to init, I think it's really important to be thorough and intentional with this design. I am still trying to understand what exactly is not possible with the fn.___=__ workaround

@davepagurek
Copy link
Contributor

I think that output is the expected behaviour -- if we used init, with code like this:

p5.registerAddon(function (p5, fn, lifecycles) {
  lifecycles.presetup = function () {
    this.globalVar += "B";
  };
});

p5.registerAddon(function (p5, fn, lifecycles) {

  lifecycles.init = function() {
    this.globalVar = "A";
  };

  lifecycles.presetup = function () {
    this.globalVar += "C";
  };
});

...then I would expect the output to be the same. So in @ksen0's example, just the assignment to fn.globalVar is the replacement for init, not the presetup parts.

@davepagurek That would prevent addons from overriding p5 functions. I do that in p5play to disable image loading/displaying as a debug feature. Also p5play's image caching and color palette system depend on it.

In another conversation we were talking about capturing the current p5 instance, is that what you're considering? something like:

function myAddon(p5, fn, lifecycles) {
  lifecycles.init = function() {
    fn.globalVar = this.someFunctionThatNeedsTheInstance()
  }
}

For context for how we're currently thinking about function overriding, in the preload addon, we also override p5 default functions, using a pattern like this:

function preloadAddon(p5, fn, lifecycles) {
  // ...
  const prevLoadBytes = fn.loadBytes;
  fn.loadBytes = function(...args) {
    if (!this._isInPreload) return prevLoadBytes.apply(this, args);
  
    const obj = {};
    const promise = prevLoadBytes.apply(this, args).then((result) => {
      obj.bytes = result;
    });
    promises.push(promise);
    return obj;
  };
  // ...
}

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

Sorry I'm getting a bit frustrated here.

I understand this is a complicated issue but I feel that I already explained it in depth, as best I can, and offered the simplest valid solution with a PR.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 25, 2025

@ksen0 @davepagurek ChatGPT 4.1 offers a better explanation than I could. Here's what it says:

The alternative suggestion of using fn.globalVar = ... as a replacement for the init hook does not work for the intended use case, for two main reasons:

  1. Timing of Global Binding vs. Lifecycle Hooks

    In global mode, properties assigned to fn (i.e., p5.prototype) are bound to the global scope after all addons are registered, but before any lifecycle hooks like presetup are run. However, the preload function (and any code that runs within it) needs access to these globals before presetup executes. This means that any initialization or mutation of globals in presetup (or later) is too late for preload to see the correct values.

  2. Instance Access and Overriding

    The init hook in p5 v1 allowed addon authors to set up instance-specific state and override instance methods before anything was exposed globally or before preload ran. The fn.globalVar workaround only allows static assignment, not dynamic initialization based on the instance, nor does it allow for overriding instance methods in a way that is visible in preload.

Demonstration:

If you try to mutate or initialize a global variable in presetup, it will not be available in preload. Only values assigned directly to fn before the instance is created are available, and these cannot depend on the instance or be dynamically set per instance.

Conclusion:

The removal of the init hook unintentionally removed this critical functionality. The workaround is not equivalent and does not support all use cases, especially those involving preload or dynamic, instance-based initialization. Reintroducing an init hook is the cleanest and most robust solution.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 27, 2025

@ksen0 Since p5.js v2 was released before this issue was solved, it made it impossible for me to add support in p5play. Now games made with p5play aren't working if they were set to load the latest version of p5.js.

https://p5play.org/play
https://game.thegamebox.ca/red_remover/
https://kevin98799.itch.io/puzzling-magnetism-recharged

Ideally this issue should've been fixed before v2 was released.

@limzykenneth
Copy link
Member

Adding the init hook does not do what you are expecting it to, assigning to this within init does not add it to global, you can try it out in your PR. In the ChatGPT summary it already mention why that is the case, global mode works by attaching p5 class members attached to the prototype (ie. alias fn) to window, assiging to this regardless of when it is done does not attach it to window (in init hook or otherwise).

If anything needs to be exposed in global mode, it must be attached to p5.prototype or fn, this is in part to create clarity on what is actually exposed to global mode intentionally and prevent unintentional global exposure. It partly applies the principle of least astonishment.

To achieve what you need here, you can do this instead:

p5.registerAddon(function(p5, fn, lifecycles){
  // This attach the variable `tester` to `window`
  fn.tester = null;

  lifecycles.presetup = function(){
    // This initializes the value of `tester`
    this.tester = "hi";
  }
});

There is no practical difference between the init hook and presetup hook, nothing that an addon can influence are between them.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Apr 29, 2025

@davepagurek @ksen0 @limzykenneth I was not aware of those changes and my assumption was that p5.js v2 still propagated props on the instance to the global scope. Although I don't like having to declare props on p5.prototype first, it's something I can workaround.

Nevertheless an "init" hook is still needed for all the reasons I mentioned previously.

I do not see the intended results with Limzy's example code. tester is still null inside preload.

https://editor.p5js.org/quinton-ashley/sketches/X6ONeT29r

Here is a demonstration with my PR that shows that tester is set to "hi" before preload is run. Remove the "init" hook and there will be an error saying tester is not a function in preload.

https://editor.p5js.org/quinton-ashley/sketches/hO-XUOV9K

I hope that this clearly shows the practical difference between the "init" and "presetup" hooks.

I seem to be facing a greater than expected resistance and stubbornness towards adding "init" hooks back to p5 v2. I don't understand why. Please have some respect for what I'm saying p5play needs and stop attempting to figure out some other way to solve this issue. "init" hooks are a familiar API for p5 v1 addon developers and "init" hooks are needed for preload compatibility, since presetup is and should remain equivalent in lifecycle order to the "beforeSetup" hook in p5 v1 which ran after preloading finished.

I'm operating in good faith here under the assumption that you all want to do what's necessary for p5 v2 and its new addon system to be able to support p5play and ml5.js at a technical level.

I'm not trying to be rude or disrespectful in any way but I simply have no interest in belaboring this issue further.

Please take some accountability and merge my PR so that I can begin to work on p5 v2 support in p5play. Many students and teachers will greatly appreciate it. 🙏🏻

@limzykenneth
Copy link
Member

limzykenneth commented May 2, 2025

preload() is not a supported feature in p5.js v2, there is no aim to restore that, sorry to sound a bit aggressive here but we have been through this multiple times already. We are also not responsible for the maintenance of p5play, I'm happy to work with any open source addon with their need but not any closed sourced code.

Unless there is clearer evidence around what the init hook adds in terms of feature for p5.js instead of to patch what p5play is not willing implement in terms of new API, I'm inclined to close this issue as this is not productive.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented May 6, 2025

preload() is not a supported feature in p5.js v2

Here's a quote from Daniel Shiffman @shiffman in regards to the issue of preload support:

My personal preference, if it's not too difficult, is to continue to support both preload() and await in the next 1.x release of ml5.js. If we release an ml5.js 2.0 someday, that would probably be a good time to remove support for preload alongside p5.js. I'm also inclined to update the ml5.js examples to use async/await by default but b/c there is a fairly extensive library of video tutorials and curriculum materials that rely on preload() I believe it's important to continue to support preload out of the box (whether with the p5.js compatibility add-on or just referencing p5.js 1.0 instead).

ml5js/ml5-next-gen#244

I fully agree. To which @limzykenneth, you replied:

I believe that is the best way forward for ml5.js as well.

So has something changed regarding support for preload when p5.js is used with addons since then?

I'm happy to work with any open source addon with their need but not any closed sourced code.

The source code of p5play is publicly available, it is not closed source.

This is the code for the entire library: https://p5play.org/v3/p5play.js

The Learn p5play interactive textbook I wrote is also free for personal use. Recently I made commercial use of p5play free too.

Selling education licenses for p5play is not a big money maker, but it puts food on the table for myself, my wife, and two year old daughter.

I also support free use of p5play by volunteer efforts such as this:

https://substack.com/home/post/p-159621537

I'm very sorry that we've had some frivolous arguments in the past but please try to take this issue more seriously.

We are also not responsible for the maintenance of p5play

Of course you're not responsible for maintenance work on p5play. I'm not asking for your help on it.

It's not that I'm unwilling to put in the effort to make p5play work with p5 v2. Clearly I am willing to do that! But I can't change games students have made in the past to not use preload.

@quinton-ashley
Copy link
Contributor Author

Ah! Now I've figured it out.

Instead of including the preload compatibility addon in p5play.js verbatim, I put p5play's init method inside the presetup hook, then add p5play's stuff to the global scope before preload runs!

@ksen0
Copy link
Member

ksen0 commented May 7, 2025

Hey @quinton-ashley glad to hear you found a solution! @davepagurek has made a new issue here #7797 to discuss possible syntax for making the ordering a bit more intuitive and not needing a workaround; as well as this issue #7798 for documenting the workaround in the meantime.

I'll be closing this issue (and the associated PR) because based on this discussion, the problem is more general than this issue; that PR does not fully resolve the more general problem. The proposed new init lifecycle hook introduces a relatively major change (the existence of a lifecycle hook that cannot be async). Since there do exist solutions that do not require this, I will close this for now.

@ksen0 ksen0 closed this as completed May 7, 2025
@github-project-automation github-project-automation bot moved this from Open for Discussion to Completed in p5.js 2.x 🌱🌳 May 7, 2025
@limzykenneth
Copy link
Member

@quinton-ashley For ml5, the comment is in the context of ml5 having one version to support both 1.x and 2.x where 1.x uses preload and 2.x uses async/await, their API already fully support promises and preload is an extra for them to work with p5.js 1.x. It was never about preload in 2.x.

If the source code of p5play is not an open source license and restrict the rights of the user in their usage, including to fork the project, it is by definition not open source. I'm not interested in discussing the license of p5play as it is besides the point and you are free to decide what works for you. The main thing is that I don't see a good reason for p5.js to support p5play specifically, considering the many barriers presented by the license, if you wish to suggest any future enhancement to p5.js I can only suggest presenting additional context beyond p5play itself.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented May 8, 2025

to continue to support both preload() and await in the next 1.x release of ml5.js

I believe it's important to continue to support preload out of the box (whether with the p5.js compatibility add-on or just referencing p5.js 1.0 instead).

@limzykenneth His clear preference would be having support for preload out of the box when ml5 v1 is used with p5 v2 with the compatibility addon, but he's also fine with just having people use ml5 v1 with p5 v1 instead.

It was never about preload in [p5] 2.x.

I got the feeling from the way that you replied in that thread that that's how you misread his comment. Thinking that way is going to limit the adoption of p5 v2 among ml5 and p5play users. You've received transparent feedback on that, yet seem fine with us not supporting p5 v2, but also you recognize that my workaround is hacky and far from ideal.

Here's the definition of closed source:

Closed source software, also known as proprietary software, refers to computer programs where the source code is not publicly available.

I didn't say p5play was Open Source™ but it's not closed source either.

I will file a separate issue, and hopefully a more robust solution to this problem can be developed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Completed
Development

No branches or pull requests

4 participants