Skip to content

Conversation

@jonbarrow
Copy link

@jonbarrow jonbarrow commented Oct 6, 2025

This started off as just an attempt to split the code based on the TODO comments present in ffl.js, but then it evolved into a basic TypeScript port as well since it seemed like this library could benefit from real TypeScript support

Completely untested right now, not even building. Literally just ports the existing JavaScript into TypeScript as 1:1 as possible. The only changes are to get the TypeScript language server in VSCode to shut up, no actual functionality changes have been added

This should absolutely not be merged right now, the old code is left as a reference for now

Changes (some of this might not be in scope for this PR tbf):

  • - Starts a port to TypeScript
  • - Addresses all of the TODO comments about code splitting, all code now lives in src split into the files the TODO comments named
  • - Add TypeScript build system (tsup?)
  • - Port all the JSDoc type definitions to TypeScript types
  • - Remove type annotations from JSDoc (TypeScript handles this)
  • - Port the missing files from materials. Some of them, like SampleShaderMaterial, do some funky factory stuff that didn't want to trivially be ported to TypeScript as-is. These will likely require some larger changes to get working
  • - Add eslint (maybe just reuse @pretendonetwork/eslint-config? I'd hate to dog-food like that and push our standards on another project, but the README did ask for scrutinization and our eslint config is based off 10+ years of working on Pretendo and other projects). EDIT: I didn't see the eslint config and it wasn't checking the src folder during the port, so I actually hadn't noticed eslint is already here. So this task can probably be removed?
  • - General refactoring? There's definitely a few areas in here that seem kinda smelly when it comes to the types/general structure of the code (such as in updateCharModel which turns off the TypeScript checker to update charModel._model, which is supposed to be readonly, and some areas do some odd line splitting and such?)
  • - Maybe suggest moving from struct-fu to the custom parser present here (once it's broken out into it's own module)? Again I'd hate to dog-food like that, but struct-fu does seem to have some weird issues with type resolution here, I keep getting StructInstance<any> results everywhere when doing this port which lacks type safety, and structs have their types defined twice (once in struct-fu and once in JSDoc) which seems to be used for adding intellisense to unpacked data? This ends up taking up a TON of space in the files, especially in cases like FFLiCharInfo, and the parser I started writing was made with type safety/automatic type generation in mind which would fix that issue
  • - Clean up imports and actually add consistency. The imports were done through a mix of manual writing and letting VSCode automatically add the imports, so things like the order of imports, the quote style, etc. are not consistent right now

completely untested and 0 functionality changes, just ports the JavaScript to TypeScript 1:1 as much as possible. the only changes made are to get the TypeScript compiler to shut up
@ariankordi
Copy link
Owner

Thanks for your effort! Anything to clean this up helps.

  • Remove type annotations from JSDoc

Nice. As for the forced JSDoc in the eslint, I realize it's pretty annoying. I like it from an AuDHD cleanfreak perspective, but definitely not necessary with TS to put it everywhere.
I'd also remove useless param descriptions like "thing - the thing", later on.

I realize not using TS just to have this much JSDoc is pretty counterintuitive. It's mostly me not wanting to leave my comfort zone.
The reasoning in my head of "screw not needing a build step!!!!!" were from Denial.. because we all know, when it comes to JS, you're doomed to have a build step no matter what 😀
I also hadn't played with JS in years and dreaded having to get into the groove with all of the tooling and ecosystem but, that's the reality of it.

  • Port the missing files from materials ...

Is the factory stuff you're mentioning the UMD? It's in struct-fu too, and those definitely have to go. This is once again my fault from being pesky and not wanting to dive into the SoyScript Ecosystem, but the workarounds for these are ugly. They can still be converted to UMD in the final build, right.

  • Add eslint

The eslint config here is actually copy-pasted from yours, but with changes including forced JSDoc.
But if your repo can be nicely "imported", I'd take that.

  • General refactoring? ... such as in updateCharModel

I was thinking of removing that outright, less "state" to worry about.

Maybe suggest moving from struct-fu

I've been thinking about doing this too, everything you're mentioning I hate as well and are things TypeScript can solve.
What I want to see is if I can do struct accesses manually with DataViews - this could suck to write, but shouldn't be that bad with no bitfields.
It'd also mean better inlining/optimization, I guess? There wouldn't be strings for each property name (oh no, gotta save 200 bytes!)
Feel free to drop your own lib in if that isn't too difficult, still better than what's here now.

I'm also thinking about how the function to convert from Mii Studio data seems sort of redundant 😔 (especially the "codec utilities", out of place for this lib. Who wrote this cr*p? 😂)
FFL itself is supposed to handle Mii data conversion, and being in C++ makes it good for that. But now here we are shoehorning it into FFL.js, and whoever is using this likely has OTHER code doing data editing or conversion too - sigh, don't know how to feel about this...

@jonbarrow
Copy link
Author

Nice. As for the forced JSDoc in the eslint, I realize it's pretty annoying. I like it from an AuDHD cleanfreak perspective, but definitely not necessary with TS to put it everywhere. I'd also remove useless param descriptions like "thing - the thing", later on.

I realize not using TS just to have this much JSDoc is pretty counterintuitive. It's mostly me not wanting to leave my comfort zone. The reasoning in my head of "screw not needing a build step!!!!!" were from Denial.. because we all know, when it comes to JS, you're doomed to have a build step no matter what 😀 I also hadn't played with JS in years and dreaded having to get into the groove with all of the tooling and ecosystem but, that's the reality of it.

I was the exact same way for years, but after making the jump to TypeScript I won't be going back. Tools like tsup also make the build step part trivial in most cases, so no worries there 👍

Is the factory stuff you're mentioning the UMD? It's in struct-fu too, and those definitely have to go. This is once again my fault from being pesky and not wanting to dive into the SoyScript Ecosystem, but the workarounds for these are ugly.

Yeah it's the UMD. My main goal for this PR was to change as little as possible in terms of functionality, just porting to TypeScript in a way that still let things build and be usable. The UMD implementation here seems to want to fight back on me with that, so it might warrant some actual functionality changes

They can still be converted to UMD in the final build, right.

They can yeah. TypeScript supports umd as a target for compiling:

The eslint config here is actually copy-pasted from yours, but with changes including forced JSDoc.
But if your repo can be nicely "imported", I'd take that.

Ah okay, sounds good then. Yes ours can be easily imported, we broke our eslint rules out into their own package a while ago so they can be easily shared between projects, which makes it trivial to import the rules anywhere else too https://npmjs.com/@pretendonetwork/eslint-config

I was thinking of removing that outright, less "state" to worry about.

Sounds good 👍 That might be out of scope here now that I think about it more, but that's good to know for the future

I've been thinking about doing this too, everything you're mentioning I hate as well and are things TypeScript can solve.
What I want to see is if I can do struct accesses manually with DataViews - this could suck to write, but shouldn't be that bad with no bitfields.
It'd also mean better inlining/optimization, I guess? There wouldn't be strings for each property name (oh no, gotta save 200 bytes!)
Feel free to drop your own lib in if that isn't too difficult, still better than what's here now.

Awesome, I went ahead and pulled it out into it's own library here https://github.com/PretendoNetwork/binary-parser. I believe I covered all the bases needed here so far? Let me know if not, I'd be happy to see about making additions. The library definitely isn't done yet (it's not even minified or anything atm lol)

You mentioned using DataView, so I feel like I should note that our library uses bit-buffer under the hood for streaming, which internally uses DataView (https://github.com/inolen/bit-buffer/blob/master/bit-buffer.js), so that area is already covered

@ariankordi
Copy link
Owner

Awesome, I went ahead and pulled it out into it's own library here

Nice, glad you released this on its own. I'm afraid to say it's too late as I finished making manual decoders for the lib. I'll definitely have a look at some point though, like, I wish I had more options before choosing struct-fu 🙂

I've been spending the past week addressing low hanging fruit in the library: using all ESM, removing struct-fu, and having no more console.debug calls.
But I've also moved to using more OOP patterns, to have less types floating around, more private "encapsulation". Running TypeDoc before and after, there's a lot less exposed and less bare functions outside of classes.
So I'm happier with what's there now, though, that much restructuring kinda messes up this draft PR 🥴



To circle back then, well I wanted to ask what your goals are here. Or maybe you just began this from impulse and didn't expect anything out of it.
Because I keep thinking about how I guess you don't "have" to care if you can import this unmodified in your own TS environment with a .d.ts, and that icon body demo should be 80% what you need out of this. This whole library using Three.js makes it easy to replace later down the line. IDK, just my two cents.

@jonbarrow
Copy link
Author

jonbarrow commented Oct 14, 2025

Nice, glad you released this on its own. I'm afraid to say it's too late as I finished making manual decoders for the lib. I'll definitely have a look at some point though, like, I wish I had more options before choosing struct-fu 🙂

I see. If bringing in the other parser is not something desired anymore, I won't push for it. Though I will say that there are still some benefits to doing so. One of the corner stones of our parser is tight integration with the TypeScript type system. That means type generation is a 1st class feature. Using our library means functions like:

/**
 * @typedef {Object} FFLiCharInfo
 * @property {number} miiVersion
 * @property {number} faceType
 * @property {number} faceColor
 * @property {number} faceTex
 * @property {number} faceMake
 * @property {number} hairType
 * @property {number} hairColor
 * @property {number} hairFlip
 * @property {number} eyeType
 * @property {number} eyeColor
 * @property {number} eyeScale
 * @property {number} eyeAspect
 * @property {number} eyeRotate
 * @property {number} eyeX
 * @property {number} eyeY
 * @property {number} eyebrowType
 * @property {number} eyebrowColor
 * @property {number} eyebrowScale
 * @property {number} eyebrowAspect
 * @property {number} eyebrowRotate
 * @property {number} eyebrowX
 * @property {number} eyebrowY
 * @property {number} noseType
 * @property {number} noseScale
 * @property {number} noseY
 * @property {number} mouthType
 * @property {number} mouthColor
 * @property {number} mouthScale
 * @property {number} mouthAspect
 * @property {number} mouthY
 * @property {number} beardMustache
 * @property {number} beardType
 * @property {number} beardColor
 * @property {number} beardScale
 * @property {number} beardY
 * @property {number} glassType
 * @property {number} glassColor
 * @property {number} glassScale
 * @property {number} glassY
 * @property {number} moleType
 * @property {number} moleScale
 * @property {number} moleX
 * @property {number} moleY
 * @property {number} height
 * @property {number} build
 * @property {string} name
 * @property {string} creator
 * @property {number} gender
 * @property {number} birthMonth
 * @property {number} birthDay
 * @property {number} favoriteColor
 * @property {number} favorite
 * @property {number} copyable
 * @property {number} ngWord
 * @property {number} localonly
 * @property {number} regionMove
 * @property {number} fontRegion
 * @property {number} roomIndex
 * @property {number} positionInRoom
 * @property {number} birthPlatform
 * @property {Uint8Array} createID
 * @property {number} padding_0
 * @property {number} authorType
 * @property {Uint8Array} authorID
 */
const FFLiCharInfo_size = 288;
/**
 * @param {Uint8Array} u8 - module.HEAPU8
 * @param {number} ptr - Pointer to the type.
 * @returns {FFLiCharInfo} Object form of FFLiCharInfo.
 * @package
 */
function _unpackFFLiCharInfo(u8, ptr) {
	const view = new DataView(u8.buffer, ptr);
	const name = new TextDecoder('utf-16le').decode(new Uint16Array(u8.buffer, ptr + 180, 11));
	const creator = new TextDecoder('utf-16le').decode(new Uint16Array(u8.buffer, ptr + 202, 11));
	const createID = new Uint8Array(u8.buffer, 264, 10);
	const authorID = new Uint8Array(u8.buffer, 280, 8);
	return {
		miiVersion: view.getInt32(0, true),
		faceType: view.getInt32(4, true),
		faceColor: view.getInt32(8, true),
		faceTex: view.getInt32(12, true),
		faceMake: view.getInt32(16, true),
		hairType: view.getInt32(20, true),
		hairColor: view.getInt32(24, true),
		hairFlip: view.getInt32(28, true),
		eyeType: view.getInt32(32, true),
		eyeColor: view.getInt32(36, true),
		eyeScale: view.getInt32(40, true),
		eyeAspect: view.getInt32(44, true),
		eyeRotate: view.getInt32(48, true),
		eyeX: view.getInt32(52, true),
		eyeY: view.getInt32(56, true),
		eyebrowType: view.getInt32(60, true),
		eyebrowColor: view.getInt32(64, true),
		eyebrowScale: view.getInt32(68, true),
		eyebrowAspect: view.getInt32(72, true),
		eyebrowRotate: view.getInt32(76, true),
		eyebrowX: view.getInt32(80, true),
		eyebrowY: view.getInt32(84, true),
		noseType: view.getInt32(88, true),
		noseScale: view.getInt32(92, true),
		noseY: view.getInt32(96, true),
		mouthType: view.getInt32(100, true),
		mouthColor: view.getInt32(104, true),
		mouthScale: view.getInt32(108, true),
		mouthAspect: view.getInt32(112, true),
		mouthY: view.getInt32(116, true),
		beardMustache: view.getInt32(120, true),
		beardType: view.getInt32(124, true),
		beardColor: view.getInt32(128, true),
		beardScale: view.getInt32(132, true),
		beardY: view.getInt32(136, true),
		glassType: view.getInt32(140, true),
		glassColor: view.getInt32(144, true),
		glassScale: view.getInt32(148, true),
		glassY: view.getInt32(152, true),
		moleType: view.getInt32(156, true),
		moleScale: view.getInt32(160, true),
		moleX: view.getInt32(164, true),
		moleY: view.getInt32(168, true),
		height: view.getInt32(172, true),
		build: view.getInt32(176, true),
		name,
		creator,
		gender: view.getInt32(224, true),
		birthMonth: view.getInt32(228, true),
		birthDay: view.getInt32(232, true),
		favoriteColor: view.getInt32(236, true),
		favorite: view.getUint8(240),
		copyable: view.getUint8(241),
		ngWord: view.getUint8(242),
		localonly: view.getUint8(243),
		regionMove: view.getInt32(244, true),
		fontRegion: view.getInt32(248, true),
		roomIndex: view.getInt32(252, true),
		positionInRoom: view.getInt32(256, true),
		birthPlatform: view.getInt32(260, true),
		createID,
		padding_0: view.getUint16(274, true),
		authorType: view.getInt32(276, true),
		authorID
	};
}

Can be reduced down to something like:

import * as b from '@pretendonetwork/binary-parser';

const ffliCharInfoParser = new b.Parser()
	.endianness('little')
	.int32('miiVersion')
	.int32('faceType')
	.int32('faceColor')
	.int32('faceTex')
	.int32('faceMake')
	.int32('hairType')
	.int32('hairColor')
	.int32('hairFlip')
	.int32('eyeType')
	.int32('eyeColor')
	.int32('eyeScale')
	.int32('eyeAspect')
	.int32('eyeRotate')
	.int32('eyeX')
	.int32('eyeY')
	.int32('eyebrowType')
	.int32('eyebrowColor')
	.int32('eyebrowScale')
	.int32('eyebrowAspect')
	.int32('eyebrowRotate')
	.int32('eyebrowX')
	.int32('eyebrowY')
	.int32('noseType')
	.int32('noseScale')
	.int32('noseY')
	.int32('mouthType')
	.int32('mouthColor')
	.int32('mouthScale')
	.int32('mouthAspect')
	.int32('mouthY')
	.int32('beardMustache')
	.int32('beardType')
	.int32('beardColor')
	.int32('beardScale')
	.int32('beardY')
	.int32('glassType')
	.int32('glassColor')
	.int32('glassScale')
	.int32('glassY')
	.int32('moleType')
	.int32('moleScale')
	.int32('moleX')
	.int32('moleY')
	.int32('height')
	.int32('build')
	.string('name', { length: 22, encoding: 'utf-16le', stripNull: true })
	.string('creator', { length: 22, encoding: 'utf-16le', stripNull: true })
	.int32('gender')
	.int32('birthMonth')
	.int32('birthDay')
	.int32('favoriteColor')
	.uint8('favorite')
	.uint8('copyable')
	.uint8('ngWord')
	.uint8('localonly')
	.int32('regionMove')
	.int32('fontRegion')
	.int32('roomIndex')
	.int32('positionInRoom')
	.int32('birthPlatform')
	.buffer('createID', { length: 10 })
	.uint16('padding_0')
	.int32('authorType')
	.buffer('authorID', { length: 8 });

type FFLiCharInfo = b.infer<typeof ffliCharInfoParser>;

const FFLiCharInfo_size = ffliCharInfoParser.size();

function _unpackFFLiCharInfo(u8: Uint8Array, ptr: number): FFLiCharInfo {
	return ffliCharInfoParser.parse(u8.subarray(ptr, ptr + FFLiCharInfo_size));
}

which keeps all the functionality, brings in type safety, and reduces the size of the section from 147 lines to just 72 (a 48% reduction). For something as big as FFL, I do think the size of the code is something to consider here so that it reduces cognitive noise for those trying to contribute/understand how the library works

To circle back then, well I wanted to ask what your goals are here. Or maybe you just began this from impulse and didn't expect anything out of it.
Because I keep thinking about how I guess you don't "have" to care if you can import this unmodified in your own TS environment with a .d.ts, and that icon body demo should be 80% what you need out of this. This whole library using Three.js makes it easy to replace later down the line. IDK, just my two cents.

Originally I had intended to see if I could get WebGPU working directly, or abstract that layer away in a way that doesn't have a hard tie to ThreeJS so another solution could be used (since at the time, rendering on the server wasn't really possible without some hacks)

After I started looking at the implementation though I saw the large number of TODO comments and decided to just try and address those. Since the code splitting was going to have to require importing now anyway, I decided it would probably be better to just do a very minimal move to TypeScript (no functionality changes) just to make use of the TypeScript ecosystem of tools for bundling (and some minor size reduction). That's what this PR was aimed at

But you've gotten WebGPU working now as per https://github.com/PretendoNetwork/mii-renderer/issues/1, so that goal isn't much of an issue now. Outside of that, I have no real goals/desires here outside of just trying to improve the tools in the scene. There are a number of TODO comments, so I thought I might as well try to address those, and I do still think there's some virtue to the idea of abstracting away the hard ThreeJS dependency but it's not a huge goal of mine or something I'd push for hard (just a "would be nice" sort of thing, in case someone else has a need for something like that)

I'm fine to close the PR if you'd rather go a different direction and let the library mature on it's own, or just wait until things aren't moving around so rapidly

@ariankordi
Copy link
Owner

Ah! I overlooked the huge FFLiCharInfo type.
Well, I can typedef it to the return type of the unpack function, and there we go, only 82 lines now 😹...

On a serious note, yes. I 100% see what you mean, this is a classic tradeoff between readability and code size. Micro-optimization. Complexity to run versus complexity to write.

If I pipe this through Closure Compiler, I get an 18 KB blob of unreadable code soup. I love that.

Writing those functions manually is not "convention" that I'd want myself or anyone else to follow. You could probably even call me a hypocrite, after calling out mii-js not using structs, and here I am doing the same lol lol....
(my defense is that this has no dependencies, while mii-js still used bit-buffer)

My perfect middle ground would be to define structs in human readable code, and generate the serializers. That C/C++ like approach is my favorite, especially if the output code has no dependencies and can be ported to another language easily.
I did look into binary-parser only after you made your fork 🥴 to see it's doing this, but only kinda. Yes it generates the functions, but at runtime using eval(?) is a little bit cringe. That may have still worked for me if I wanted to port the struct-fu structs (or ask AI to), but...
What I chose to do in the end is to vibe code something that'd take struct-fu definitions and make those deserializers.
Even that was pretty rough though and I touched them all up manually. That, as well as dwarfdump, which is a nice tool to show you struct fields if you haven't seen it. (I may even base a tool off of this? 🤔)

dwarfdump for FFLiCharModel

image

So if I fixed that script, defined all structs in JS, and then built them to an autogenerated untouched file included in the final build, I'd love that. Will I do that- probably not with this project.
The tradeoff for readability is something I accepted mostly from not seeing a huge future or expandability here, since it's tied to the limitations of FFL. As I read that back to myself I realize that's not a great excuse actually 😔

For something as big as FFL, I do think the size of the code is something to consider here so that it reduces cognitive noise for those trying to contribute/understand how the library works

Great point here. Here's what's coming to my mind...

  • Given the layout/naming/usage of structs is the same, IDK if I would say it's significantly less readable if the definitions are still in the C++ code or dwarfdump.
  • If someone wanted to make the library work in another language that doesn't have struct support, it's probably easier to use those manual decode functions as a base.
  • In this case, the structs FFLiCharInfo/FFLiCharModel are just implementation detail of FFL that would help someone understand the library, but not necessarily what the library is doing. It's "abstraction".
    • Aaactuallyyy.. come to think of it, none of those were originally public. Devs are expected to call functions to get DrawParams from the model, and CharInfo is totally internal - you just give the library your Mii data, it handles it under the hood. I'm just using it as a way to convert from Studio data while retaining Switch colors. I'm then reaching into FFLiCharModel to get the raw faceline/mask DrawParams, as the FFL library usually draws them internally. You see why I call this thing walking tech debt?
    • If I added more functions to the C++ code to access the structs, integrating support for Switch data too, it'd probably avoid the need to access them from JS.

Originally I had intended to see if I could get WebGPU working directly, or abstract that layer away in a way that doesn't have a hard tie to ThreeJS so another solution could be used ...

Everything you said makes more sense from this perspective, yeah. I guess it's just a shame FFL is the only accurate option right now. If I didn't already mention it, Three.js's abstraction would make it easy to replace this library or write your own.

Outside of that, I have no real goals/desires here outside of just trying to improve the tools in the scene. There are a number of TODO comments, so I thought I might as well try to address those

That's good of you to do. I keep going back and forth about splitting and TS, "more maintainable and less tech debt" vs "smaller and simpler to use" 😔 I guess with this specific project I am "single file C header"-pilled.

I'm fine to close the PR if you'd rather go a different direction and let the library mature on it's own, or just wait until things aren't moving around so rapidly

🙂 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants