• he/him

real official verified page of Bekoha (real) at cohost dot org


JillCrungus
@JillCrungus

I wanted to give a quick status update on what I've been working on in regards to Psychonauts, since it's been a little while.

Brain Tumbler

A large chunk of my time spent working on Psychonauts stuff since last time was spent on working on a Blender addon (titled "Brain Tumbler") designed for use alongside Oatmeal to make working with Psychonauts-specific data in Blender easier. If you read my post on making custom levels, you'll know that "Brain Tumbler" was originally what evolved into Oatmeal. The original iteration was planned to be a full suite - importer, tools and exporter.

In this new design the responsibilities are split - Oatmeal is handling format conversion, leveraging my existing PsychoPortal library. Oatmeal embeds Psychonauts-specific data into glTF extra data but I can't well expect prospective world builders to edit that data by hand to set up flags and such. Thus, a Blender addon is necessary.

Brain Tumbler is that addon. Inside Blender, Brain Tumbler presents a nice UI for setting and viewing this extra data. It can import/export the extra data set up by Oatmeal (in a JSON chunk called "lipo_data") to be used during conversion.

This split responsibility approach is ideal and far, far better for me personally than previous plans. If Brain Tumbler had been a full suite then it'd have presented the problem of me essentially needing to either find a way to use a .NET module in Blender (I tried, it's not good) or entirely re-implement PsychoPortal in Python (not happening). Now, with Oatmeal using PsychoPortal for conversion, all I need is some glue code in Brain Tumbler to translate specific things to Blender.


Here's some examples of what some of the components of Brain Tumbler look like right now:

Material and Collision Editors

An important couple of menus for sure. These are both integrated as part of Blender's material editor.

The material panel is for, well, editing materials. Right now it only supports a few options and doesn't provide much in the way of actually defining traditional material properties but the essentials are there.

The collision panel is for when you're building your level's collision mesh. My rationale for these being per-material is that collision flags need to be set per-face on the collision mesh and, well, the only way to really do that is with materials. To handle this during conversion of Psychonauts data to glTF, Oatmeal figures out every unique permutation of per-face collision data in the mesh and creates materials for each. Anyway, this is where you set your collision data including footstep sounds and also "trigger surfaces" which are only really used for a handful things and most of them are for level/entity defined behaviour.

All the flags (where their behaviour is known) have documentation when hovered. Some flags don't have known names or functionality yet, and it's possible some of them are even just completely inert and leftovers from earlier in development. Fun! Part of the job here is figuring that stuff out eventually.

Entity Editor

Appears when you're editing empties, which are used to represent entities in this case. There's not a lot to it - level entities don't actually have a lot of Psychonauts-specific data.

Classname is a path to the Lua class to use and editvars are used to set per-entity values (highly dependent on your Lua class, if I ever write some basic tutorial for building levels I'll have to explain these. If you've used Source, editvars can be thought of as the entity's keyvalues.)

And... that's kind of it, right now. There's also preliminary support for lights which don't have a unique editor and mostly just translate from Blender's built in light values. Technically, there's also a small custom editor panel for bones but it's only used to flag which bone is to be considered the root of the skeleton.

If you're wondering why it seems so sparse, it's because I predictably needed a significant break after crunching out over a week of nonstop work to get custom levels working in time for the game's birthday. I actually built the birthday level without really having Brain Tumbler in place and then started working on it some time after I posted the birthday celebration post. I ended up getting far too tired to work on it and so ended up putting it down for a bit. In that time though, there is a an annoying thing that has not really left my head:

The Domain and Scene problem

Psychonauts' engine is a scene based one. It's not unlike an engine such as Godot - the whole world is a scene, entity meshes are scenes. You can theoretically load multiple "world" scenes at once if you're so inclined, or load Dogen's model as a world (which will not work out because he has no collision data but it works in theory). But there's more at play here. Scenes can have "runtime references" to other scenes to load (which might have had more relevance in development but in the final game it basically just means that some levels are split up into multiple subscenes) and I'm not sure how to handle that.

Thing is, gltTF does also support the concept of scenes. As does Blender. You can build out something using multiple scenes in Blender and export it as glTF and it'll work. Great! Except... import is broken. Blender's glTF I/O plugin knows about multiple-scene glTFs, can export them, but cannot import them. On import, a multi-scene glTF will have everything merged down into a single scene. Turns out this is a workaround for a bug that dates back to Blender 2.8. Oops! The bug is gone but the importer was never fixed, and remains unfixed to this day, so I'm going to have to find a workaround for it.

And then there's domains. Domains are like big boxes within scenes which contain sets of entities and meshes. Oatmeal is cheating with this hard right now and shits out every mesh of every domain into a separate glTF, and has no support for creating PLBs with multiple domain. It takes 2 glTFs as input (mesh and collision) and just dumps them into a single root domain. I have no idea how to handle this honestly. I could just add a new "Domain" data type for empties and use those, I suppose, but domains purely use bounds to define their area and position/rotation/etc. information is irrelevant to them so I'm not sure. I can't use collections because those just become glTF nodes if you export them and turn into empties on import.

One thing I have considered is changing how Oatmeal works - I'd still support direct usage of glTFs for stuff like entity models and the most basic of basic levels but I've considered having it export to and support conversion from JSON files. Oatmeal would use this to store and retrieve information about the scenes and domains in the level. Brain Tumbler would then define custom importers and exporters for Oatmeal JSON files, manually set up the scenes to work around the issue mentioned previous and translate the domains to Blender collections. This would work around both issues but would also be contingent on me figuring things out like how to invoke the glTF importer/exporter from my own importer/exporter. I think it's doable though.

Thing is, I'm not artist or level designer so I've been kind of winging it with how I build this and just doing whatever seems right. But I'm a programmer, so I understand that the solutions I come up with are probably borderline insane to actual artists and level designers. As such, if anyone who's reading this is an artist or designer who has thoughts on everything I've talked about so far, thoughts on how to handle some of the problems I've mentioned and so on, I'd love to hear them.

Astralathe

All this work and excitement doesn't mean Astralathe hasn't been getting some love. In fact, returning to some work on Astralathe is one of the main things that inspired me to post this status update.

I was recently working on an unrelated project for a different game and had the thought to integrate Dear ImGui into it. I'd tried this before with Astralathe without a lot of success and I sort of gave up on it after some time.

However, while working on it for this other project, I learned about some facilities that the hooking library I'm using (PolyHook2) has that I was previously unaware of. This actually made it very easy to hook into Direct3D and DirectInput in that other game and I'd written the code for it in such a way that I was actually able to copy it over directly to Astralathe and get it working with just a few tweaks (namely a couple differences in how Psychonauts handles input compared to the other game, as well as Psychonauts using D3D9 and not D3D8 but it wasn't too much work to fix those parts).

As such, I've been having quite a lot of fun playing around with the possibilities. I'm hoping that the inclusion of an in-game UI will allow me to create better configuration tools for Astralathe as well as useful developer stuff. I'm also hoping that now that I'm hooked into the rendering process I can consider restoring some of the debugging tools that were compiled out of the Windows release of the game.

Here's an example of a functional Lua-based debug console:

And this is a mostly non-functional mockup of how a mod management menu could look:

An actual user-friendly system for setting up your mod list like this is something Astralathe has always been sorely lacking and a large part of that has been my absolute inability to do UI design. ImGui is very nice though, and quite programmer friendly which makes it quite easy for me to build even a menu like this.

The functionality half of this menu is quite continent on a rework of Astralathe's current mod handling system though. Right now it has no way of loading all this fancy information, or handling dependencies, or anything like that.

I've also been playing around with really digging Astralathe's claws deeper into the game's inner workings. Here's my debug scratchpad window (mainly for personal use, quickly testing stuff, etc.) where I've set up an entity list.

This is actually being done by hijacking the game's own internal entity iteration system via what I've been calling Class Proxies and Var Proxies. Class Proxies are essentially just simple wrappers that take a pointer to a class and hold Var Proxies which know the offsets of certain fields and provide ways to access them without doing unhinged shit like this:
(char*)( ( (char*)g_pGameApp ) + 39066 )

Instead, I can define a ClassProxy for GameApp that can wrap around g_pGameApp and give easy access to fields.

class GameAppProxy : public ClassProxy<GameApp>
{
public:

	VarProxy<GameApp, EScriptVM*> m_ScriptVM;
	VarProxy<GameApp, char*> m_pszCurrentLevel;

	GameAppProxy(GameApp* d) : ClassProxy<GameApp>(d),
		m_ScriptVM(*this, 39476),
		m_pszCurrentLevel(*this, 39066)
	{
	}
}
syntax highlighting by codehost

Which I can use like this:
GameAppProxy(g_pGameApp).m_pszCurrentLevel.GetValue();

I'm sure this is nothing particularly new but it was a fun system to hook up and should save some pain in the future.

I'm also not entirely sure how this works out performance wise (I can't imagine it's too bad, as under the hood it's still doing the same offsets and casts), and I'm not sure if there's a better way I could be finding the field offsets rather than hardcoding them, but I think I'll likely have to just live with this for now.

With this I was able to write a proxy for the game's EntityIter class used to iterate the entity list, and even set up the proxy in such a way that I can construct my own EntityIter via proxy, making this entity list very easy to make.

if (ImGui::BeginTable("##entList", 2))
{
    EEntityIterProxy iterator(nullptr, true, false);
    int i = 0;
    while (iterator)
    {
	ImGui::TableNextColumn();
	ImGui::Text(iterator->Name.GetValue());
	ImGui::TableNextColumn();
	ImGui::Text(iterator->IsDomainSleeping() ? "Sleeping" : "Awake");

	i++;
	++iterator;
    }

    ImGui::EndTable();
}

Anyway, that's enough nerd stuff. The point is that Astralathe is getting better and better and it's very exciting. I'm hoping I can do some fun stuff with it down the line. We haven't even covered what could be done when I start extending the game's Lua API with new functionality. Mods could register their own configuration options that could be changed ingame via the ImGui menus, for example.

That's it for the status update. Tune in... uh... some other time, I guess. Whenever I next feel like talking about this stuff. Yeah.


You must log in to comment.