Oh, whoops, hadn't thought of that. Next update will allow SetThingPuppet(thing, -1); and SetThingSoundClass(thing, -1);
Note that, like with ParseArg for puppet=none, -1 will only remove the animclass and not the internal puppet (the thing that manages the 4 keyframe animations); so it'll still be possible to play keyframes on the thing manually. If you want to totally disable animations, you'll need to do something hacky like SetThingModel(thing, SetThingSprite(thing, LoadSprite("bubble.spr"))); which I'm pretty sure would work...
I just figure matching a vanilla behavior makes the most sense.
Also, I haven't seen anything else noteworthy that went unused in JK COG stuff. I suspect the "NR" verbs were originally "no return" and didn't put a return value on the COG stack, because I'm also pretty sure that earlier in development the COG stack had to be carefully balanced. But released JK doesn't care and throws out excess COG stack values (which there are generally plenty of because more verbs have return values than is documented, e.g. ChangeInv) and all(?) NR verbs were ultimately given return values anyway so are either literally equal to their non-NR variants, or functionally equal.
QM
Edit: Okay, here's what I consider an important JK update (and also some more cogext stuff):
https://drive.google.com/file/d/1nFFNxfKMmxyTMjLZ98Boi0XLN_OkQrox/view
https://drive.google.com/file/d/1PSKDygKl2WEyyW58oVG8k0Zu7cto3s3C/view
First, for cogext, the changes for SetThingPuppet and SetThingSoundClass to allow -1, and some new verbs:
jkStringConcatCogName();
jkStringConcatTemplateName();
jkStringConcatSoundName();
jkStringConcatMaterialName();
jkStringConcatModelName();
jkStringConcatKeyframeName();
jkStringConcatAIClassName();
jkStringConcatSpriteName();
jkStringConcatPuppetName();
jkStringConcatSoundClassName();
jkGetFrameTime(); returns int
jkGetAlpha(); returns int
jkGetBlend(int); returns 4 bytes packed into one int (e.g. hex 0x01010605, int argument should be 0-4)
The jkString verbs can be used like:
jkStringClear();
jkStringConcatAsciiString("puppet=");
jkStringConcatPuppetName(GetThingPuppet(player));
ParseArg(thing, ÜÜ);
For JK.exe, thanks to the OpenJKDF2 project, I adapted what they did to fix COG string corruption when loading a save.
More importantly, I fixed multiplayer surface textures getting out of sync (which is especially a problem if say, one player is using JKE for model and texture replacements, and another player isn't). As a bonus to this, save files are also basically always much smaller now (e.g. 400 KB down to 150 KB).
So, JK uses the same data structures both for its save data and for its network packets. In multiplayer, it tries to only send packets when something changes. When saving, it iterates through everything in a level and saves out an update "packet" for each, so:
- almost all things, their puppets (the 4 animation controller) and what they're attached to
- all AI objects
- all COG objects, both level and static (e.g. 00_door.cog vs items.dat)
- all surfaces (even ones that haven't changed; this is often the bulk of the save)
- all sectors (even ones that haven't changed)
- all inventory info for the player
- all timers (I'm not positive what these are, could be COG timers)
- all screen tints
- all cameras
- all sounds
- a bunch of important static variables
In vanilla, all surface-altering COG verbs set a "this_surface_has_changed" flag on the surface they alter and queue up a max of 32 surfaces per network update to sync up. In single player, that flag is not set. So, I changed it so that even in single player they DO set that flag, then when a save occurs, it only writes out packets for the changed surfaces. When loading, the game doesn't care if there's an entry for each surface or not, it just reads what's there and applies packets to surfaces it has packets for.
I also modified the surface packet to include the name of the texture (mat), the same way model packets include the name of the model (3do), to avoid unmatched indexes between players.
I did all of this in a backwards compatible way, so vanilla can understand saves and packets made by JK 2023, and vice versa. The multiplayer benefit will only work if both players have JK 2023, otherwise, they both get vanilla behavior.
There's one gotcha: if you load a vanilla save with JK 2023, then, before finishing the level, save and then load that new save, some surfaces may be in an incorrect state for the rest of the level. This is because vanilla didn't set the "this_surface_has_changed" flag on anything, so when JK 2023 goes to save, it won't know which surfaces have and haven't changed. I do not consider this issue to matter.
A possible issue is there may be some surfaces that should be saving out their data that currently are not, but that will get improved by learning what's been missed. Emphasis on may; I see some theoretical issues (e.g. changes to adjoins don't set the "changed" flag), but I don't know if that matters.
Edit 2: SurfaceLightAnim likely isn't saved, but I'm not positive it truly saves in vanilla either; I think it winds up being a SetSurfaceLight that doesn't set the "changed" flag.
Edit 3: cogext 2023 experiment 9:
https://drive.google.com/file/d/11oAE7fvUm6riNdprWWHa7QLhuJ1KS2gW/view
Fixed (implemented, really) ChangeModelMaterial(model, index, material);
Added GetModelMaterial(model, index); returns material
Examples:
Edit 4: cogext 2023 experiment 10:
https://drive.google.com/file/d/1VWs5GfEvuuQaoRSev0_rYiuAXt965QJG/view
Implemented CloneModel(model); returns model
Added IsModelClone(model); returns 1/0 true/false (true = model is a cloned model)
Level model limits still affect this, so making lots of cloned models will quickly exhaust available model slots.
In multiplayer, setting a thing to a cloned model will make other players set it to the original model the clone is based on. I think. That's how it should work anyway. I'm not far enough along to have tested to be 100% sure.
If you want to sync cloned models in multiplayer, you'll need to perform the SetThingModel(thing, CloneModel(model)); in a non-sync'd COG (cogflags 0x200), then SendTrigger a custom trigger to instruct the other players to do the same. And you'll probably need a join message to send newly connected players the same instructions. So not MP friendly, but possible.
I think I need to do a little more work on the hierarchy section at the end of the model data to be 100% correct, but what I got working doesn't blow up or anything.
I suspect you can guess what's coming next now that CloneModel actually exists.
Edit 5: JK 2023 204 and cogext 2023 11:
https://drive.google.com/file/d/1IwhKLFyOYgkYeXuETzYxhOySU6zjxmD5/view
https://drive.google.com/file/d/1sLObdcz_n9J7PL3feQQzlN1KmkobQ-Kz/view
For JK 2023, solved the vanilla save loaded by 2023 then resaved and reloaded surfaces issue.
For cogext, added two new verbs:
GetModelMaterialCount(model); returns int, for iterating through model mats in COG
SwapModelMesh(model, mesh_name_or_index, model, mesh_name_or_index);
The swap is fast and won't crash or hang your game. I mean probably; always the chance I messed up, but testing went well.
Also cleaned up the hierarchy section for CloneModel.
I thorough analyzed SetModelMesh and SetThingMesh. Both are faulty (obviously we discovered that empirically, but I plumbed their depths). I will be rewriting both, and both will get their mesh name argument enhanced to allow a mesh index. And I've got verbs in the works to start with a joint number (e.g. 0 is head) and find its associated mesh index within a model, so after another update (or a few more updates) you'll be able to do something like:
The verb names aren't final. And no promises I'd make the compact helper versions. For example, in the above example, a hypothetical GetThingJointMesh would do something like the following:
And I'm not positive that'd be a valid for loop in COG.
Edit 6: JK 2023 205 and cogext 2023 12:
https://drive.google.com/file/d/1fTq82018kHlpTelVK11MrTgR4HKM9Caf/view
https://drive.google.com/file/d/1GGCXZbCgDrpODZZv9GIrYcYAOYVmPZrC/view
Not time for mesh manipulation yet.
Quick definition of terms:
- static - will refer to resources (3do's, MATs, KEYs, etc.) loaded when the game first starts, generally stuff referenced in static.jkl, but also materials on models loaded by static.jkl are also themselves loaded at startup and are then also static, and maybe stuff referenced by static COGs in their symbols section?
- dynamic - will refer to resources loaded by a level, and unloaded when you change / exit levels
You should be able to check if any resource in COG is static (even in vanilla, I didn't add this) with code like:
if(GetThingModel(player) & 0x8000) Print("static");
The index numbers of resources have 0x8000 or'd into them if they're static.
Realized there are issues if you put dynamic materials on static 3do models. Both via ChangeModelMaterial or SwapModelMesh. So now there are safeties in place:
- You can put static materials on both static and dynamic models. (ChangeModelMaterial)
- You can only swap meshes between static models or between dynamic models. (SwapModelMesh)
SetModelMesh will follow ChangeModelMaterial rules. SetThingMesh will have no restrictions because it will automatically clone the thing's model (once, will reuse the clone on subsequent uses on the same thing). I'll likely also add a ChangeThingMaterial that auto-clones the thing's model. Again, these verbs aren't rewritten yet, so don't use them yet.
CloneModel produces a dynamic model. So clone a static model if you want to swap its meshes with another dynamic model. The list of static models in vanilla is small, but includes ky.3do.
Anyway, onto the change log:
The update to JK 2023 was to add an index number to particles (specifically particle clouds, PAR files) so they can be used in COG (sanely).
The update to cogext was to add those aforementioned safeties (restrictions) to SwapModelMesh and ChangeModelMaterial and to add the following verbs:
GetThingParticle(thing) returns particle_index (or -1 if not particle cloud)
SetThingParticle(thing, particle_index) returns previous model/sprite/particle index (and frees the thing's puppet like SetThingSprite)
LoadParticle(filename_string) returns a particlecloud index
jkStringConcatParticleName(particle_index)
jkStringConcatModelNodeName(model, hierarchy_node_index)
jkStringConcatModelMeshName(model, mesh_index)
There is no (sane) way to save out mesh and material changes, so for good single player support, a COG would need to detect a game load and re-apply changes. An easy way to check for game load when working with CloneModel is to check the thing's model and see if it matches its unmodified model, so like if(GetThingModel(player) == ky_3do) { // reapply changes here. In multiplayer, if trying to sync model changes, a join message to send new players the changes should be a viable approach (which would then be carried out with custom SendTrigger messages); though keep in mind that model and material index numbers wouldn't be in sync, so you couldn't just send model and material numbers themselves in the trigger, so the design would need to be where the trigger specifies a "change number" that locally specifies the changes to make.
Brainstorming, templates internally are things, so it may be useful to add template get verbs, like GetTemplateModel, and so on. So you could do GetThingModel(player) == GetTemplateModel(GetThingTemplate(player)), etc.
Edit 7: Okay, this is a major one, JK 2023 rev207, cogext 2023 rev15:
https://drive.google.com/file/d/1wJOxB0CWFANKFD22tZMhb7RngmLHmWp9/view
https://drive.google.com/file/d/1f3A8p0X5HmuSOUli1sMszbhrGU4Ze19l/view
Modified JK to allow COG access to templates as if they were things and fixed the multiplayer character preview rotation speed.
For the template access thing, it works like:
GetThingModel(GetThingTemplate(player) | 0x80000000);
You or 0x80000000 with the template index before passing it to any verb that can take a thing. Even Set* verbs. Some may have no result. Some may blow up. Experiment.
Probably lots of ways to crash or at least break the game if you misuse this, but also super easy and useful when used responsibly.
Now for cogext. First off, SetModelMesh and SetThingMesh are now functional and safe to use. SetThingMesh is the same as:
Plenty of new verbs:
jkStringLowerCase(); converts the current jkString to lowercase, useful to do a case insensitive comparison with jkStringEquals
jkStringUpperCase(); converts the current jkString to UPPERCASE
jkStringEquals("string"); returns 1 (true) if the current jkString exactly matches the supplied ascii string, technically allows matching wide characters above 0x255 with a ?, jkStringEquals(ÜÜ) should always return true since it's comparing jkString against itself
jkStringWildcard("s?r*g"); filename style wildcard matching, ? matches a single character, * matches 0 or more characters, due to the specific way I wrote it, *? will need to match a literal ? character, think of it like an accidental escape sequence
GetPuppetJointNode(puppet, joint_index); returns node_index
SetPuppetJointNode(puppet, joint_index, node_index); untested, probably works
GetModelNodeMesh(model, node_index); returns mesh_index (which can be -1 for none)
SetModelNodeMesh(model, node_index, mesh_index); untested, probably works
GetModelNodeCount(model, node_index); returns int number of hierarchy nodes in a model
GetModelNodeParent(model, node_index); returns the parent node_index of a node
GetModelNodeChild(model, node_index); returns the first child node_index of a node
GetModelNodeSibling(model, node_index); returns the sibling node_index of a node
GetModelNodeChildCount(model, node_index); returns the number of child nodes of a node
GetModelMeshCount(model); returns int number of meshes in a model's geoset 0, and I think all geosets need the same mesh count to be valid
GetModelGeoSetCount(model); returns int number of geosets of a model (1-4, maybe can be 0?)
An example use of some of the new stuff: