Reface - restore classic decal faces following the Dynamic Head Migration. | v1.1.0

Reface [rbx-reface]

Reface (or rbx-reface) is a module for restoring Classic Heads and Faces following the Dynamic Head Migration.

Reface automatically detects migrated dynamic heads (and shapes) and swaps them back to the correct classic mesh + face decal, with full support for R15 and all head shapes.

Installation

Reface is available via the Wally package manager:

[server-dependencies]
reface = "secret-rare/rbx-reface@1.1.0"

And as a .rbxm file via GitHub.

Creator Store availability is something I’ll work on soon - need to figure out the exact workflow to automatically publish it following a GitHub release (psst, a pull request would be welcome for this if anyone wants to do my work for me…)

Usage

Just one line of setup: call reface.init() once from a server Script. That’s it!

local reface = require(game.ServerScriptService.Packages.reface) -- or wherever the module is located

reface.init()

You’re additionally welcome to use the submodules rbx_reface provides for your own character loading workflows - rbx_reface/convert’s convertCharacter(humanoid) method is exposed, along with others, if needed.

Notes

  • Reface supports currently unsupported head shapes (hex, diamond, etc.) and falls back to the former default smile face - it’s not possible as of right now to get faces back on these heads, and it’s possible it never will be :frowning:
  • Reface is early in development. You may encounter edge cases or missing features. Bug reports and contributions are very welcome!


Reface is a project of Filoxen Labs, an organization interested in the history of online social games.

9 Likes

question, specifically about this modules methodology on replacing dynamic heads:

what happens if roblox deprecates the functionality of humanoidDescription.Face and humanoidDescription.Head, which is likely to happen considering they are properties purely reliant on classic items, those of which have been archived? this module provides a fix for that by never daring to rely on humanoid descriptions, allowing for significant longevity.

additionally, does this modules database automatically scale as roblox adds more dynamic heads? this and this both incorporate that functionality by using a http-based database. though, the prior is community-driven, and anyone can contribute their own databases, url-chained with a main database.

2 Likes

If that happened then I would move to just replacing the mesh of the Head manually and instancing a decal - I’m not sure they will do this but if they did it’s an easy fix. I only chose to use HumanoidDescriptions since it’s the currently correct way to do it - if they deprecate those properties, obviously a workaround will be required

On your module and the fdh one: I’m not convinced that on Roblox, HTTP reliance is the best route, especially for large, production scale systems - it’s best to avoid making unneeded external calls, especially to sources you don’t control. Reface’s database doesn’t “automatically” scale per se, but if I recall correctly the number of unconverted faces as of now is pretty low? As soon as new ones become available the codegen scripts are pretty easy to rerun and create new mappings. Also, your implication seems to be that an HTTP-reliant database is any more “future-proof” and community-based than just a regular Git repo? If for some reason, I’m unable to maintain reface anymore, or am not adding a feature, forks and pull requests have long been tradition in such situations all across the FOSS community, haha

in my opinion it’s a bad habit to not get something right the first time- like to plan ahead. murphy’s law. i lean strongly on the side that they will deprecate .Face and .Head and then push some future update which further breaks their functionality.

my module addresses the amount of ‘unneeded external calls’ with federation mode, which aims to only use httpservice once per game and relying on messageservice to get the database from the other servers. this becomes somewhat of an issue with larger filter lists and small games. in addition, users are incentivized to use their own fork of pre-existing filter lists if not able to trust my own, and this is done with just inserting a new url into the ‘lists’ array in my configuration.

personally, i disagree that http-reliance is not the best route for this type of system, because:

  1. its seamless and takes little to no time (especially in its own script/vm which is how my module is intended to be used by-design + requesting from raw github which is blazing fast). i belive you’re kinda implying it takes a while
  2. it’s transparent
  3. it’s cheap
  4. it has the potential for a community-driven approach
  5. it auto-updates for games sharing the same lists

as for trust, there are other systems which follow this sort of methodology. on my phone, i use rethinkdns, and set it up so im using automatically-updating dns filter lists to block out different sites. on my router i do the same but with adguard home. these are standardized methods of using these particular services- contributor’s http-based filter lists, and not once have i witnessed a community-owned filter list being abused to for example block a shit-ton of typical sites. i guess the question really is, are the people behind these lists trustworthy? if they aren’t, its an easy fix to remove the offending list; does trust necessarily play a role in which one should use upstream databases?

to address the latter, its possible to add explicit parameters per upstream list. for example, being able to mark a dynamic to classic filter-list claiming to only affect classic heads as ‘noUGC’ and/or explicit allowing ‘changeHeadMesh’ and ‘changeFaceDecal’ with syntax.
e.g

# a chained filter list in my own module
link http://somefilterlist.com {UGC=FALSE; CHANGEHEADMESH=FALSE; CHANGEFACEMESH=TRUE}

i find that roblox’s limitations are friendly with http-based filter lists like the two modules i lined.

your codegen script is cool as fuck :white_check_mark: . but the thing is, your user-base wouldn’t know when to change these mappings, making it inconsistent for devs per game. having what i expect 100s of people updating their conversion lists for a new roblox face is not going to work very well. especially if you intend to implement ugc-support, which is also where an http-driven approach thrives. also, not everyone has py or pip3 or whatever the fuck on their computer, and so your module in terms of updates wouldn’t be drag-and-drop.

naw i use a github repo as my http database

yuh

As it stands, the “right” way to do this is using HumanoidDescription. If this changes, I will change with it. I’d rather not hack around engine features when they exist. If your module wants to be opinionated to do so, feel free!

As for UGC support or entirely new dynamic heads - I’m not planning to do that, as this module aims solely to restore Roblox created classic faces. If UGC is something your module can do, awesome! I’m not sure it’s a missing feature though.

The federated system your module uses just feels overengineered for what is essentially a table that maps from around 600 Mesh IDs to 600 Decal IDs. I’d prefer to keep it simple and use a hard-coded mapping that I can change if I really need to. That being said, this is a thread about my module, and not yours, so I’ll end that discussion here. Feel free to make a PR if you see any viable ways to future-proof Reface that don’t involve implementing new & complex systems, like a custom tokenizer to parse number strings.

2 Likes

im just gonna briefly reply:

my bad i didnt mean to imply ugc was a missing feature in your own module. it’s a completely unnecessary feature

my module is as u said opinionated, its planning for the future and to be as scalable and as longevous as humanly possible given roblox’s track record of removing what the community loves most. federation mode helps scalability by reducing http calls. yeah, its just a map at its core but it builds on that idea to be fundamentally intangible to roblox.

its for the file format that i made up to be as beginner friendly as humanly possible (in pursuit of ease for me and also the community)


i don’t necessarily agree that mine is over-engineered, just not lightweight. over-engineered is an imbalance of code and functionality, like printservice. every configuration in my module is intentional and makes massive differences in its design and usecase.

yeah of course i can do a pull req or two :+1:

4 Likes

you’re the goat thank you for this

I can’t see how your module isn’t over-engineered relative to the use-case. You chose to build a custom lexer/parser, for a problem that could have been solved with a much simpler data format. That is a disproportionate amount of complexity for something as straightforward as restoring classic faces onto a character.

I do understand the intent to future-proof your module against future Roblox updates, and I do agree that planning ahead is the right move here. Ironically though, the way you’ve done that is more fragile than necessary, considering a good percentage of this module is essentially reimplementing what Humanoid:ApplyDescription already does at an engine level.

There are also places where your custom tokenizer is entirely unnecessary, for example, you wrote a function solely to extract numbers from a string, something that’s already been easy to do within Luau using built in functions like string.gmatch. This is overkill.

local function getNumbersOnly(targetString: string)
    local numbers = {}
    for num in string.gmatch(targetString, "%d+") do
        table.insert(numbers, tonumber(num))
    end
    return numbers
end

It’s good that you kept beginners in mind when writing your module. That said, if a user can understand how to swap out the filter list, they can also probably use a simple table format (players are already used to doing this with admin scripts!), JSON, or even CSV. All of those would be easier to implement, maintain, and read than building a whole tokenizer/parser/etc for a basic config format.

dyn 115499438051492 face 25321744 # Friendly Grin
dyn 15554865353 face 20722053 # Shiny Teeth
dyn 13682084865 face 6531805594 # Award-Winning Smile

Can’t you already parse this kind of data using basic pattern matching or string manipulation? I’m curious why you chose to write a basic language for this.

Lastly, if readability and future-proofing are your goals, then simplicity matters even more. The easier something is to understand, the easier it is for others to contribute to and maintain. Even as someone who is more comfortable with reading code like this, I found the codebase difficult to follow. That kind of complexity is a barrier to contributors. Sometimes the best solution is a simple one.

4 Likes

Your sentiment is valid. I can see why over-HTTP may not be the best, but if you really do think about it: things will come and go.

My initial thought when working the module was to allow a scalable database as a temporary solution in this early stage of analyzing missing assets to patch leftovers ASAP.

I think it’s better to say that two libraries have different priorities; nothing is better than the other. It’s still quite early to assume that it is safe to use static database. Over-HTTP libraries can be used for a time. Once we are clear on the news that a public record has cleared it all, it will be deprecated, and we will switch over from the clunky HTTP-reliant dynamic database to a hardcoded one with no delay.

2 Likes

If I could give my honest perspective, I feel as though using JSON would’ve probably been much more friendly for the commons. Back then, before I knew what the dang Lua was, I knew how to use JSON and changed local game data.. I think this is not just a me scenario but a universal knowledge. It has clear boundary on what is scoped, and the pattern is repetitive, so there is no fear of the unknown.

Likewise, I was approaching the documentation from an open learning mindset and struggled to understand how a single head works with multiple faces with the syntax that was used as an example, even if I really wanted to see how your system can be incorporated in games.

Bet no fear on JSON if you’re doing a double take.

1 Like
long reply

some bits are fragile, specifically in ‎replaceHead, and others are workarounds to get around roblox’s bugs. for example, i dont know if you saw this bit:

local requiresNeckSignal = humanoid:GetPropertyChangedSignal('RequiresNeck')
neckBefore = humanoid.RequiresNeck
humanoid.RequiresNeck = false
while (humanoid.RequiresNeck) do requiresNeckSignal:Wait() end

during play-testing with the module, i found that roblox would occasionally kill the humanoid during the head replacement module because it wasn’t, i guess, registering the requiresneck property quick enough (during total head replacement)? maybe this doesn’t work, maybe it does- i’ve found no issues with using this line though. and the user could also use fakeheads which are far more reliable.

if (not isBanished) then
	-- this line below fixes an issue
	while (marketInfo.classic == 0) do marketInfoLoaded.Event:Wait() end
	return replaceHead(character, marketInfo.classic, player)
end

this code is also the result of what i presume is a bug. unavoidable.
there’s also this bit, going back to replaceHead, where i actually re-weld everything to the new head and parent everything to a new head. it’s been sketchy but has worked perfectly. i don’t think theres another scalable alternative for this thats as cohesive but not as sketchy.
i don’t know if this hacky workaround is what you meant by fragile. there’s also a potential memory leak with the federation mode reading over the code now, and i need to double-check if traverse is faster than getdescendants for example. there are also other large bits about the module that are absoutely robust.

i could use string manipulation for this part, and its probably wiser to for the current use-case. however, i designed this module with flexibility in mind, and found that using string manip would somewhat go against that. i also decided that because i’d already made the tool to allow getting the numbers manually (the custom token/parser), i would use it for consistency-sake. this aspect of my module is definitely harder to justify, but philosophically i believe it makes sense in pursuit of a long-term solution. the tokenizer is (as u said) more-so necessary for the actual file-format. this numberonly is not overkill at all from a readability perspective, it is slightly less readable than the gmatch alternative. it is however overkill from a memory and performance perspective. my module is actually designed to be unperformant though so its cool.

ease of implementation is the least of my worries for this module and i definitely wanted to use json at first. there’s plenty of tools that make editing json easier, its well-known, and it’s pretty. i even implemented a json parsing mode at the first stages of the module. i removed it, and decided that it would be simpler to have my file format as word number word number et cetera. frankly, scratch all those extra symbols, which are syntactically error-bound for beginners. they are not necessary for the module. it’s definitely counter-intuitive, potentially sacrificing work-flow for simplicity for beginners.

i see the point of cutting down on lines of code to allow easier contributions and better maintainability.

i decided against using a simple table format to tailor to those who do not know how use a simple table format but are capable of using a filter list (entry of quotations with a url, followed by a comma; cfg.lists)

i could. one of the biggest reasons why i didn’t is because i didn’t want to hardcode string formats into my code, and i think that’s perfectly valid. another big reason would be to introduce more functionality later down the line if i find it necessary. a third reason is certainty- by using the tokenizer/parser, i know for certain what it’s doing and what are its capabilities and limitations. you can definitely argue that by saying to just learn regEx by heart, which is known to be stable, and that’s definitely possible. but i feel as though regex, being based on very rigid and convoluted strings formats, would actually limit what could be done with my module. i should definitely add more commentary throughout my code to counteract this, and actually make a documentation, regardless.
one thing that isn’t as valid is not using string manip for the individual tokens, etc numbers, strings, words. personally i found, and you can definitely prove me wrong, but it’s more-so complicated to do that while respecting the comment formats my file format allows and also the new-line functionality.

in general the file format is too complicated for reg-ex and i don’t want to put an obscene amount of time into maintaining regex.

i’ll definitely do some revisions with how i present my code, and i can possibly consider using that gmatch thing you said for the ugc description ids. i’ll definitely add more comments. the language module is mostly easy to understand (just a bit of mental maths) so there shouldn’t be any issue there. everything’s named appropriately in that language module, and most of the functions in that module are positioned appropriately; however, the main code should definitely be revised in terms of structure and nomenclature. the language module feels fragile, and some of the main code does as well. most of the module (around 70%) shouldn’t be difficult to understand, not sure where you’re finding difficulty since its very high-level (in terms of as a prog. lang) and also not very algorithmic or mathematical. the federation mode is a bitch for me to even understand though, that’s completely reasonable. so is the filter list compilation functions approx ln 500 alongside its respective functions. i’m not claiming you’re a liar, and theres obvious bias in my judgement, but starting from the bottom makes the flow a lot easier to understand.

in conclusion of my long-winded post, a good portion of what you mentioned is intentional design. especially the tokenizer w.r.t my way of thinking. i see some revisions to make though. thanks regardless, your insight is valued

strange and bearthegruff add me on discord. ilucere

bearthegruff

valid take, but my file format is also repetitive and there is not much fear of the unknown once the syntax is learned. i dont know if json would be more friendly, i guess its not a matter of which is more friendly since they’re both pretty straight forward. i opted for my format to absolutely dumb it down. i wanted accessibility paired with my modules degree of robustness.

for example

head 0 -- head defaults to the one in cfg.defaultHead

dynamic 953774834 face 34733543 -- some hated dynamic head and some face decal
-- new entry: {head=0, dynamicHead=953774834, face=34733543}

dynamic 9876543 face 12334567
-- new entry: {head=0, dynamicHead=9876543, face=12334567}

head 123
dynamic 4385928358 face 348958923
-- new entry: {head=123, dynamicHead=4385928358, face=348958923}

-- all further entries will use 123 as the head mesh id

so that multiple faces can be used with one head mesh. pun intended this is called header-style.
but i should make mechanic this more clear in the documentation, thanks.

1 Like

Oh ja, and on this, this is also a specific issue I faced when trying to implement R15 support. It is fine on R6, but it becomes an issue on R15, and I guess that it’s one of Roblox’s dirty tricks to suppress any attempt on replacing dynamic heads.

RequiresNeck is an interesting workaround, perhaps, might be even shorter. I figured that using an RBXM model template wasn’t really the best in methodology and storage on my end and that I should use ID instead.

Unfortunately, since MeshParts are read-only, you couldn’t just instance.new() a MeshPart and edit the ID and call that a day, and it will lack the proper translation if you do, so I dug a deeper research and found that InsertService:CreateMeshPartAsync() exists. Then, I used CreateMeshPartAsync() to create the MeshPart with proper translation and apply the MeshPart to head with Head:ApplyMesh().

1 Like
reply

my issue statement was really vague and poorly phrased ngl. i meant to say:

when the player spawns, they would occasionally instantly die during total head replacement rather than fake head allocation; explicitly polling requiresneck was a fix i thought

nope they would start with ‘deprecating’ humanoiddescription.face and humanoiddescription.head. roblox is just buggy- i worked around at least 5 roblox bugs in my converter

i thought about it- requiresneck probably doesn’t work. what you’d want is to wait for the neck motor6d, and then poll for both assignations of p0 and p1

i used specialmesh’s but i will probably switch to meshparts so i can have consistent sizing, good idea

I want to go back over a few things here since there’s been a lot said.

Before we even discuss HTTP databases and federation and file formats - after testing, your module doesn’t support head shapes at all. Players can own non-standard head meshes and use faces with them following the migration - like Roundy or Perfection - and Reface detects those and preserves the correct head shape when applying the classic face1. Your module’s database sets head 0 and gives everyone the default round head regardless of what they actually had equipped. I think getting the actual data right matters a bit more than the architecture of the system you use to deliver it:

On the “planning ahead” thing - you’ve said relying on HumanoidDescription is a bad habit and that you “never dare to rely on humanoid descriptions.” But you do use them? In dynamicFaceCheck you call humanoid:GetAppliedDescription() to read humanoidDescription.Head to figure out which dynamic head the player has. If Roblox deprecates2 those properties, your module breaks the same way mine would. The difference is that fixing it in Reface is a few lines of code, and fixing it in yours means reworking a 1,200 line file. I’m not sure the future-proofing advantage here even exists the way you think it does

On HTTP/federation - as I’ve stated, the problem we’re solving is mapping ~500 dynamic head IDs to classic face and mesh IDs. It’s a finite, slowly-growing dataset. It’s not like DNS blocklists where you’re tracking millions of constantly-changing values across the entire internet - comparing this to AdGuard Home and RethinkDNS doesn’t really work because that’s a different problem. A hardcoded lookup table doesn’t need federation because there’s nothing to fetch or propagate. The whole “scalability” question just kinda disappears when the data exists already

On the file format being “beginner friendly” - beginners already know JSON, CSV, or Lua tables. Your format requires learning a custom syntax, which is arguably less intuitive than any of those. And it required a 485-line parser to support, which is a barrier to the contributors your module is supposed to attract.

You said your module isn’t overengineered, just “not lightweight,” and defined overengineering as “an imbalance of code and functionality.” By your own definition: Reface does this with ~70 lines of logic. Yours uses ~1,950 lines of hand-written code including tokenization and lexing and doesn’t support head shapes. You also said “my module is actually designed to be unperformant though so its cool” and I’m not really sure how to respond to that one honestly… why would that ever be a goal?

I’m not saying your module has no merit, but I think it’s kind of ridiculous to come into a thread for my module and show off yours and how much better it is when it lacks feature parity with Reface while being overengineered. I want to end this here, so I won’t be responding to any further argument.

1. When possible - some heads like Hex don’t have support for faces and default to the original smile face
2. It’s also important to note that rarely on Roblox does “deprecation” mean “removal”. Speedy removal of deprecated features is typically reserved for safety issues - there is no safety issue relating to HumanoidDescription instances. This doesn’t mean that continued use of a hypothetical deprecated property is good, but it would not immediately break either module (and likely never truly would “break” them)

2 Likes

im gonna keep this brief. your points are insightful but also a few of them are at their core criticisms of my modules philosophy, which i should definitely make clearer on the readme.md. at the end is a very fundamental issue in your module.
this whole blob of text below is not an argument its a defense addressing your claims.

p means paragrpah f means footnote

p1 - you’re right, i didn’t realize roblox still supported head shapes. thanks for pointing that out. can you share with me the bundle id of that dynamic head please?

p2 - i don’t rely on humanoiddescriptions- my module replaces the head in totality. i use humanoiddescriptions to check whether the figure has a dynamic head, because checking the meshid of the head is inconsistent between r6 and r15

p3 - my module is concerned with the growing population of dynamic heads.

p4 - the syntax is dead simple. i don’t understand the argument of assuming beginners already know json, csv, and lua tables. especially since beginners that know these three formats will easily grasp my own. you said arguably, but its not at all less intutive than any of those three. the 485-line parser is a valid argument but it’s also pretty clear code-wise especially when i add comments and documentation. it also has the potential for expanding its syntax if necessary for a new feature, which is pretty sweet.

p5 - you misinterpreted my definition. there is a lot of functionality with my module to allow it to be implemented in any project with a strong degree of control. i recommend you check the (poorly-written) docs/api.md. the module unperformant thing is obviously a joke; but also a slight truth considering the usage of http and optional federation. its not a goal its a byproduct of my modules philosophy

p6 - in other words cut down the code and fix the head mesh issue whilst maintaining functionality?

f1 - :+1:
f2 - that’s where we disagree fundamentally- roblox is seeking to push dynamic heads, and i believe the options for having classic head and face properties on the humanoiddescription will be inevitably deprecated and/or unfunctional in due time, or even potentially removed. only time will tell. i never said anything about humanoiddescriptions themselves.


my point of being scalable / production-ready stands here. on me and my family this bit is not to slander your module.

reface, after calling .init()


tfog:

both of our modules seem to have issues. but it seems like while my issue is size and the head thing, and your pretty sweet future-proofing argument which i’ll heed to; yours just didn’t work at all.

real life scenario: my projects structure is complicated, and having to troubleshoot reface is not in my interest. i’m seeking a drag-and-drop. there were no errors in the console or anything.

local function onPlayerAdded(player: Player)
	player.CharacterAppearanceLoaded:Connect(function(character: typing.CharacterModel)
		print('a')
		convert.convertCharacter(character.Humanoid)
	end)
	if player.Character then
		print('b')
		convert.convertCharacter((player.Character :: typing.CharacterModel).Humanoid)
	end
end

adding prints (i dont personally use breakpoints) in your playeradded function shows convert.convertCharacter doesnt run at all. characterappearanceloaded is annoying to work with in general too. so, knowing characterappearanceloaded is just terrible, and assuming the character wasn’t registering as loaded at all, i tried this:

local function onPlayerAdded(player: Player)
	player.CharacterAppearanceLoaded:Connect(function(character: typing.CharacterModel)
		print('a')
		convert.convertCharacter(character.Humanoid)
	end)
	if player.Character then
		print('b')
		convert.convertCharacter((player.Character :: typing.CharacterModel).Humanoid)
	else
		player.CharacterAdded:Wait()
		print('c')
		convert.convertCharacter(player.Character.Humanoid)
	end
end

‘c’ prints, which is great-

however, my characters face regardless stays this stupid-looking

our modules have their differences especially in philosophy. but while the bare minimum for me is making head meshs translate properly, your bare minimum is having reface work for large projects and also having it work for normal avatars. it doesn’t account for the default head mesh, and so everyone who’s used reface at this point will have this bug in their database, potentially unknowingly.

this is where an http-driven approach thrives- its as simple as going into a url and appending a value which accounts for the default head mesh. also this hasn’t been brought up yet, but this is where a version checker like my module has, also thrives.

as a sidenote: your module should be drag-and-drop by default in my honest opinion. in my opniion, having to call init is an artificial way of giving the user control.

other side note: i really conflated humanoiddescription.head in this post i believe. this is shown with your paragrpah #1. is desc.head it not used for dynamic heads? or is it for putting both a regular head mesh and dynamic head mesh? the latter is a quick fix on my part

other side note: i’d recreate this forum post and delete/hide this one. while this is a pretty productive debate, sorry for bloating your post.r

I’ll respond to the last bit here since it’s a genuine bug report - I can’t repro this.
Your HumanoidDescription.Head is 0, (which is a failure on my part to consider - will push a fix for this today) but the mesh should still be defaulting to the intended classic head mesh. Do you have any sort of custom character loading code outside of this? This is one thing I’d like to improve support for; .init() is really only usable if it’s the only code that alters character heads. Otherwise, directly calling convertCharacter within your character loading code is the best way to go.

My repro attempt (tried on both R15 and R6 with 0.1.7):

1 Like

yes my game uses a blank startercharacter and for now i’m loading the players avatars humanoiddescription onto it
thats the character loading in a nutshell. (temporary, not stupid)

players.PlayerAdded:Connect(function(player)
	local i = info(player, {
		worldState = simpleStates.New(),
		listeningHosts = {}
	})
	
	i.localDataHandler = dataHandler.New(player)
	i.localData = i.localDataHandler:Get()
	
	pingS:Start(player)
	player:LoadCharacter()
	
	socket:Fire('localPlayerAdded', player)
	--task.delay(2, player.LoadCharacter, player)
end)
players.CharacterAdded:Connect(function(first, player, character, parts)
	--...
	i.lifeMaid = maid.New()
	i.character = character
	i.body = parts
	
	
	connectDeath(player)
	loadPlayerAvatarFull(player, character, parts)
--...
end)

mind responding to the 2nd sidenote too?

i was under the impression that dynamic heads were immutably face+head, not that the head itself was changeable.

and also p1

the circular dynamic head with the epic face.

As of the migration update, head shapes can be selected from a variety of classic heads - that specific head is Perfection. See the Head Shapes on the left here:

The new versions of head shapes don’t have bundle IDs as far as I can tell, but the bodyPartDescription.HeadShape property of the BodyPartDescription corresponding to a character’s head gives a string corresponding to the selected head shape.

On your character loading pipeline - it seems like this is a case where directly calling .convertCharacter somewhere in the pipeline after loading would work. I’m not sure this is a design issue but just two incompatible systems; the Reface daemon expects traditional character loading, which you aren’t using. The conversion logic still works here, just not automatically as it does for some other use cases.

1 Like