Massassi Forums Logo

This is the static archive of the Massassi Forums. The forums are closed indefinitely. Thanks for all the memories!

You can also download Super Old Archived Message Boards from when Massassi first started.

"View" counts are as of the day the forums were archived, and will no longer increase.

ForumsJedi Knight and Mysteries of the Sith Editing Forum → Jedi Knight Quadranscentennial Edition
Jedi Knight Quadranscentennial Edition
2023-10-04, 10:03 PM #41
It's both definitely possible that I broke something, and that layering these patches made a mess; you should be applying any of these recent ones to an otherwise unpatched JK 2022 executable. Obviously a bit of an oxymoron (unpatched patched jk.exe).

Using the extracted JK 1.01 executable, put it in the bsdiff folder of the JK 2022 setup files (_setup\_tools\bsdiff), then you can manually patch it to a "clean" JK 2022 using:
bspatch.exe jk.exe jk.exe patch\JK.1_01_to_220409.patch

From there, you can apply JK_linuxinputsleep.ips and then set aside that copy of jk.exe as your clean base to apply any of these test 2023 versions I'm posting IPS files for.

Anyway, I'll take a peek and see what I broke. I assume it was something when hooking up the new hotkey COG stuff.


Edit: With a quick test, comparing JK 2022, rev185 and rev187, I didn't notice a difference in turning behavior, mouse or keyboard. =/
I'll investigate more tomorrow. I did notice issues with the frametime command and keyboard turn speed that shouldn't occur, so will also be looking at that.

Edit 2: While I don't know exactly what you are seeing with messed up mouse turning and such, I did identify problems with OpenJKDF2's input framerate compensation for keyboard that I'll fix. I also found a mistake on my part involving fixing high framerate speedup (this wasn't a huge issue, but I simply noticed it while looking at the input stuff).

Edit 3: For the mouse, do you have "Map directly to axis value" enabled? Because that's at least one cause of stuttery mouse movement with missed input.

Edit 4: Okay, new version:

- SubModeFlags 0x80000000 now just disables mouse high framerate compensation because keyboard is back to vanilla behavior. So you probably won't want to use it anymore.
- Framerates over 100 now should correctly not cause the game to speed up.
- Framerates below 5 (200ms frametime) will now make the game slow down instead of vanilla's 2 fps (500ms) threshold, because physics was breaking too hard below 5 fps. It's still pretty rough at 2 fps. The ideal would be to implement a fixed physics interval where missed frames run multiple physics passes, and maybe extra high framerates aggregate physics passes, but that seems overkill.
2023-10-05, 9:53 PM #42
Fun fact, the mouse issue is not present on rev190, as it wasn't on rev185. It was exclusive to rev187, even with a "proper" installation, so the issue is apparently solved somehow.

And yes, I've always used Map directly to axis value and never had an issue with it.

Edit.: Nevermind, the issue is still present. It's just... less annoying? less inconsistent? idk
2023-10-05, 10:28 PM #43
That's a good lead at least, "Map directly to axis" and compare against rev185.

For what it's worth, historically I used "Map directly to axis" because mouse sensitivity didn't go high enough, but I patched in a higher sensitivity limit and the mouse definitely works more accurately with that setting off (at least on Windows).


Edit: Though, I would appreciate it if you can double check rev185 just to be positive occasional dropped mouse input doesn't happen, because for me that's definitely a vanilla JK issue with "Map directly to axis" mouse. Like, if it exists and isn't actually new, would be the same subtle(ish) bad behavior as rev190.
2023-10-05, 10:56 PM #44
In fact, turning off Map directly to axis (and setting sensitivity all the way up to 370) did fix it. Odd, never had that issue before. If it happened with versions prior to 185, it's sporadic enough to be imperceptible.

I'm kind of not even sure what that toggle even does other than apparently making sensitivity super high, but I guess my problem is solved
2023-10-06, 11:35 AM #45
It changes how JK handles mapping mouse movement to in-game actions (e.g. turning, pitching). It isn't as complex and loses some precision; with "Map directly to axis" on, the minimum amount you can turn increases with the axis sensitivity value, and very slow/small mouse movements can end up being ignored.

The issues aren't in your head though, and "Map directly to axis" behavior is different between 185 and newer iterations, and working through this I found JK to have a left turning bias (given equal amounts of left and right turns, you slowly turn further left over time). So all stuff I'll work on.

The 370 max sensitivity is because that's the number of notches on that sensitivity slider. Any higher and you wouldn't be able to pick a number accurately. As far as I recall, you can set an even higher value if you edit the sensitivity setting directly in a player\Name\Name.plr file. 370 is arguably at the threshold of usable on the lowest sensitivity mouse I had on hand.


Edit: Okay, figured out the "Map directly to axis" issue; the continuous hotkey code was trying to grab an axis number value off the stack where a raw axis read function (sithControl_GetAxis) was clobbering it (using it as a temporary variable), so a little tweak to code fixed that. This mattered even if you weren't using a COG hotkey message.

Edit 2: Okay, there's also an up bias. Basically, the negative direction (left/up with no inversions) is highly sensitive to minimal movement, while the positive direction (right/down) "rounds down" towards zero, so there's a threshold before a minimum 1 value occurs. Trying to figure out if this is a DirectInput flaw (it sorta seems like it, and I've even seen people bringing up the issue as recently as 2018 in other game engines), something in JK's logic as the axis values work their way up a chain of functions, or even a mouse hardware / driver issue.

Edit 3: Whew, looks like it was just a mistake in vanilla JK code. It looks like they were trying to average mouse inputs when framerate was high (two frames within 24ms, > 41 fps) to make it feel smoother or avoid jitter... or something. Figuring out if the logic can be salvaged or if it should just be bypassed.

Edit 4: Nope, the vanilla logic is garbage. My best guess is they were trying to smooth jitter that might happen with a spiky framerate, but it did not work in a worthwhile way. Mouse aiming suddenly feels so much better with that resolved. Posting a new version soon.

Edit 5:

Edit 6:
Finally fixed high frametime waggle.

Edit 7: Have been digging into what it would take to let AI both walk on walls and be able to look around somewhat correctly. I think I have the logistics worked out, and a side benefit would be adding support for turnright, buuut... I've hit a roadblock in not having an adequate testing scenario. I neither am set up to make a test level, nor was I ever very capable at it, and attempts to find a surrogate to test in have been mostly fruitless.

So, is there any chance I could convince you to put together a minimal test level? I'm imagining a single spherical sector (or, y'know, sphere-like by JK standards), with one NPC that walks a looping route, and one that either chases, flees, or ideally alternates? They need to target the player since changing facing is important to get things tested and fixed, and again, alternating between targeting and not targeting would probably be even better. Ideally actually doing minimal damage (1? 5?) so that I have time to turn on invul, but still able to kill me if I want to test that, and with enough health that I can rail det them at least 2 or 3 times to see how they behave when a force detaches them from a wall? Oh right, and obviously with the right physics flags, probably something like 0x42DF.
2023-10-10, 1:45 PM #46
That is surprising and spectacular. Yes, will do that ASAP.

hopefully this will do
2023-10-10, 3:18 PM #47
Excellent. Thank you. Already found some issues I wasn't able to encounter with my previous test location (which is good).


Edit: Mainly just to have it written down somewhere while I'm working on it, in addition to dealing with the AI turning code that resets them to upright, I also had to deal with AI target setting code that normally sets the target Z position to the actor's Z position rather than their target's (unless the actor is flying or in water), with movement code that zeroes their added Z velocity (if they're not flying or in water), and code that bends their torso (and such) to look up and down not accounting for orientations besides upright. Things are coming along nicely though; I have that test guy following me up the wall and across the ceiling now, he's just not looking towards me correctly all the time yet. I expect once I have it working, it'll require some real-world testing to find anything I'm not accounting for.

Edit 2: Okay, here we go:
- actors with physics flag 0x80 set will set target positions that include their target thing's position Z vector
- actors with physics flag 0x80 set won't zero their chase velocity Z vector (i.e. they will try to walk up walls and on ceilings when chasing their target)
- actors will maintain their up vector (uvec) when turning so they don't snap upright
- actors will pitch relative to their up vector (uvec) instead of the world up vector (0/0/1)
- actors with zero yaw (y) rotational velocity (rotvel) will have -0.0 yaw if turning right (in JK yaw left is positive, right is negative)
- actors with negative yaw rotational velocity (rotvel) will attempt to play their turnright animation if they have one, and fall back to vanilla behavior (turnleft without a check) if they don't

Note that physics flag 0x10 isn't a requirement, nor is clearing physics flag 0x800, but both are suggested. Also note that actors, like the player, will still jump upwards according to global up (0/0/1) instead of their up vector (uvec).

Edit 3: Just a small update to finish optimizing the AI turning code to offset the slightly more computationally complex AI pitch code:
2023-10-16, 12:00 PM #48
Wonderful. This actually goes far beyond what I imagined was possible with these patches. I initially thought it would be nice to have 0x10 physics flags working as kell dragons and other larger/flatter enemies look stupid walking on sloped faces, but you went all the way and made wall walking possible, which was something I didn't know I needed.

I did notice two tiny issues, though: First, I'm pretty sure turnright and turnleft are switched for actors. Second, it's probably very hard to reproduce but the player model in JKGR (which is actually a ghost actor) sometimes won't stop the turning animation after turning. This usually happens when I move both pitch and yaw at the same time, and I've yet to test if it happened on rev194 or if it appeared with 195.
2023-10-16, 2:36 PM #49
There won't be any differences between 194 and 195. The optimization was cutting out some wholly unused code that was calculating then discarding the result.

In pseudo-code, actor animation logic is:
if(thing->thingtype == TYPE_ACTOR && thing->actor != null)
 if(thing->actor->flags & AIMODE_TURNING)
  animation = 31; // which should always be "turnleft"
  animation = 1; // "stand"
else { player stuff }

I modified it so instead of just animation = 31, it checks if the actor's rotation velocity Y value (yaw) is negative, and if so, verify it has a "turnright" animation defined, and if so, animation = 32.
if(thing->thingtype == TYPE_ACTOR && thing->actor != null)
 if(thing->actor->flags & AIMODE_TURNING)
  animation = 31; // "turnleft"
  if(thing->physics.rotvel.y & 0x80000000)
   if(thing->animclass->modes[thing->puppet->majorMode].keyframe[32] != null)
    animation = 32; // "turnright"
  animation = 1; // "stand"
else { player stuff }

I tested in game that the animation = 32 code fired when the test gran actor turned right to face me, but I can go double-check.

I sorta expected a turning animation would be replaced (shut off) by whatever logic normally governs ending it, as I'm just changing which value (31 or 32) to use when an actor turns, which is governed by the AIMODE_TURNING flag. I don't know exactly how you're using your player actor thing, or if that complicates the matter or not.

Hm, the AI turning code decides it's done turning when the desired look vector matches the current look vector, with a little tolerance, and that could be what's failing. The AI turning code is really dumb and could theoretically overshoot and never be close enough to its desired look vector to end. That was among code I changed for this, and it could be where the "stuck turning animation" problem is; it should be the same as vanilla when an actor is upright / gravity-aligned, but a mistake on my part or a floating point inaccuracy issue are both possibilities.

How are you setting the actor thing's look vector?


Edit: Ok, went and verified some stuff:
- that turnleft is index 31, and turnright is index 32, and stand is index 1
- that vanilla AI animation code sets their animation to 31 if AIMODE_TURNING (and 1 otherwise)
- that the test gran triggers the AI turning code as expected, regardless of its up vector, which sets rotvel.y to -0.0 if turning right
- that the test gran triggers the AI animation code as expected, setting animation to 32 if rotvel.y is negative, and to 31 otherwise

The vanilla AI animation code is slightly tricky and converts AIMODE_TURNING true to 30, and false to 0, then adds 1 to either, so I stepped through both situations in a debugger to make sure I wasn't misunderstanding it.

It's hard to determine if the test gran ever gets "stuck turning" because he punches, and pushes me, and is generally so aggressive about moving that if he does get stuck turning, he quickly gets unstuck or another animation hides it.

Edit 2: Oh dammit, of course... it's the player where turnleft and turnright are backwards. The stupid game plays the player's turnleft animation when they're rotating right (turning right, as any sane person would describe it) and vice versa.

The player's pup (ky.pup) even makes this clear with its animation filenames:
turnleft        kyturnr.key     0x01    3       2       # submode 21 = shufflin' left
turnright       kyturnl.key     0x01    3       2       # submode 22 = shufflin' right

Well crud... I guess it's time to dig through NPC pup files and see if any have separate turnleft and turnright animations defined, and if they're inverted or not. The gran has both defined but to the same animation file.

Edit 3: Okay, a single pup file, su.pup, which should be for the Drugon fish-type water enemy, has a turnright AND has different turnleft and turnright animations, and left is left and right is right (unlike the player). ATST's appear to have turnleft and turnright animation files, but don't use either in their pup file.

So, what makes sense?
1) Have actors work logically, where turnleft is for turning left, and turnright is for turning right?
2) Or follow the messed up player logic where turnleft is for turning right, and turnright is for turning left, but at least you could use the two interchangeably?

I actually think 2) is what makes sense, especially if Get/SetThingPuppet are implemented.

Edit 4: Looking at the Drugon animation in the Puppet Jedi utility, it sure looks like suturnl.key turns right, and suturnr.key turns left, and quickly implementing 2) above checked out in-game. So now to just figure out the stuck turning issue. I really need a reproducible test case. =/

Edit 5: It's a 1-byte change by the way:
offset 0xE9E4A change 0x73 to 0x72
That'll invert actor turnleft and turnright use.
2023-10-16, 7:10 PM #50
Making it wrong so as to match the player's seems like the way to go.

Regarding the non stopping turning anim, here's what I found on closer examination:

The actor thing will almost always get stuck in the animation played when turning left (at this point idk whether that's turnright or turnleft), but very rarely it's the other one. It happens only when pitch exceeds a certain value (either positive or negative). Upon closer inspection, it happens whenever the thing (not head) pitch and roll appear as different than 0 when using coords cheat (which is probably related to rounding errors when converting R/L/U Vecs to PYR?). I can't tell whether it's more closely related to pitch or roll as both values always seem to be the same whenever I rotate. However, I can tell that the higher either these values, the lower the head pitch "threshold" for getting the anim loop. If the values equal 0 it will never happen.

I can't tell what causes it to get stuck in the anim for turning right, but it happens very rarely.

This is a simplified code for controlling player body's look direction in JKGR (variable names were replaced for better readability)

edit.: I got the wrong portion of code, corrected

// repeat every frame
        bodyPYR = GetThingPYR(player), lookPos = GetThingLVec(player);
        lookThing = FireProjectile(player, ghostTpl, -1, -1, VectorSet(0, 0.15, next*0.0001), '0 0 0', 1, 0x0, -1, -1);
        lookPos = GetThingPos(lookThing), DestroyThing(lookThing);
        for(i=0; i<numParts; i=i+1)
            SetThingPYR(bodyPart00, bodyPYR);
            AISetLookPos(bodyPart00, lookPos);
        next = !next;
That "next" variable is there to make sure head pitch is updated every frame so as to keep all body parts aligned in case I change the 3do to one of them.

It's probably something to do with SetThingPYR, I'll try something with that and see what happens. Either way that probably shouldn't be happening.

edit2.: In fact it doesn't happen if I replace SetThingPYR with SetThingLook, so it's most certainly related to imprecision with the RLU to PYR conversion.
Maybe I'll have to use SetThingRLUVecs instead of SetThingPYR? bummer.
2023-10-16, 7:42 PM #51
Don't make any changes on your end yet (or at least not irreversible changes). I need to look through those verbs and make sure that there isn't something I missed, scrutinize the new AI turning code and determine if it's an accuracy issue or math or logic flaw (which presumably would be fixable if so) and it'd be ideal if your code stays as-is to test potential fixes.

For example, if it's an accuracy issue, I can change the AI "done turning" check when they're not upright to something better (while leaving logic when upright as-is so it matches vanilla).


Edit: Oh right, and I meant to ask if there are any things, vanilla or COG extensions, that you don't use because of crashes? Like, multiple COG extension verb descriptions say they crash on level end, but I've not done any testing to confirm, and am curious if there are any you want to use but don't due to said crashes.
2023-10-16, 8:09 PM #52
Regarding crashes, SetThingMesh() is something I'd REALLY like to use but while functional, it always crashes on level exit and its best replacement, SetModelMesh(), which I do use, crashes on game exit - as in the game closes but the process freezes and doesn't end on Windows and on Linux it just displays a crash message whenever I close the game.

I believe SetGameSpeed() is supposed to change by how much "slow motion" slows (or speeds up?) the game, but it crashes on use. GetGameSpeed() seems to work as expected and returns 0.2. The same happens with SetPlayerRespawnTime(), while its counterpart GetPlayerRespawnTime() also seems to function as expected.

Also, the following verbs sound useful but they either don't work or I don't know how to use them (or have no idea what they're supposed to do)

Edit: Also, if we're talking new cog verbs, two verbs I'd REALLY like to have (beyond the aforementioned Set/GetThingPuppet, Set/GetThingSoundclass and maybe Set/GetThingSprite) are HeapSave("filename.ext"); and HeapLoad("filename.ext");, so I could finally drop the JKGR external .exe launcher + process memory hack shenanigans

Edit 2: also, something like ScaleThing(thing, vectorXYZ) and GetThingScale(thing) would be nice to replace SetThingRLUVecs() for thing resizing. ScaleThing() could take a vector to indicate by how much to multiply the thing's (normalized) R, L and U vecs by, and likewise GetThingScale() could return a vector with the lengths for the thing's R, L and U vecs.

Which reminds me, touching a thing with a R, L or U vector of length != 1 causes forces to be applied in a very inconsistent manner, as if said thing had a velocity even if it's stationary. Maybe too difficult and too marginal an issue to fix, but it happens. There's also some absolutely bizarre behavior when you give certain things a collide type of 3 and touch them? It's been long since I messed with that so I can't recall any details, but I remember it would even move the camera around far away from the player thing.

Edit 3: Apparently collide type 3 kind of works for actors in that it changes their collision bounds in some way. On the player however it just seems to make the camera behave bizarrely. That's probably a non-issue though.
2023-10-16, 9:43 PM #53
Wait, what? HeapSave and HeapLoad should be COG extension verbs. Like two of the originals that were added... oh... they were removed at some point. Uhm. Ok. I can probably rectify that. Since a kind Massassian just dug up early cogext source code for me that still has those two verbs. Looking at them, they're dead simple. I assume they were removed out of safety concerns. Pretty sure I can come up with something that's "safe enough."

So, big IF on if I tackle COG extension stuff but...

I had looked into puppet, soundclass and sprite support a little and a glaring issue is that COG doesn't include them in its resource type system. This means they can't be defined in the symbols section and can't have typed variables for them. Not saying it won't be possible, but doing anything beyond bare integers (basically ONLY Get/Set and no symbols support for pre-initialized variables, which is "good enough for limited use" but not especially flexible) would require quite a bit of effort. I think it might also require modifying the save format for 100% full support.

Does GetThingHeadLVec work? I have notes I took years ago that says it didn't. A COG of mine used GetThingHeadPitch and calculated the HeadLVec manually.

At a glance, it's not obvious what's wrong with SetGameSpeed... I wonder if that memory address isn't writable. That'd explain the crash. Ah yep, that's it, it's in the read-only data section.

SetPlayerRespawnTime is the same but worse, it's not just in read only data, it's in code; it's trying to modify part of an x86 instruction to change the player respawn time value.

The proper way, if programming JK 26+ years ago, would be for both to use static variables in writable memory. The nasty way that I'll likely do it to avoid relocating them is to make the COG extension verb momentarily make that memory writable, make the change, then put back the write protection; it's sorta odd that the COG extension code didn't work that way already.

GetPOVTilt should return a vector, and SetPOVTilt should use 3 integers and set the third person camera angle maybe? Not sure why it's 3 integers and not an input vector, but eh. Those functions "work" but I don't 100% know what the vector they're changing does, it's just something used by the camera code. Default values appear to be 5.0, -8.0 and 8.0. The 5 seems to be the main value (a multiplier), and -8 and 8 are min and max after multiplication?

Oh yikes, right, you can do goofy things by setting RLU vectors. And can make them non-normalized... crud, need to look over that new AI turning code stuff since it all assumes an always normalized thing up vector, need to see if that matters.

While there'd be convenience in Scale verbs, there'd be little else... the same heavy math would have to be done regardless (e.g. VectorLen on each vector to get the respective RLU scale). What are you hoping for out of those?

Not sure on the Mesh and Model verbs. And yeesh regarding the collision stuff. Haven't looked at that code, so no comment at the moment.


Edit: Brief notes:
ChangeModelMaterial(model, indexOfMaterialInModelFile, material);
So, like:
material kyfacgrn=kyfacgrn.mat local
ChangeModelMaterial(LoadModel("ky.3do"), 0, kyfacgrn); // replace kyface3.mat
Which I assume would be a visible face change. The code looks functional.

CloneModel() is unimplemented; takes no arguments, does nothing, returns nothing.

Edit 2: Oh wait, SetPOVTilt doesn't work. It takes 3 integers as arguments but does nothing with them. Probably because the values it needs to write are in read only data and trying would just crash. Also, integers isn't a good data type anyway; it should use floats (flex) or just a vector.
2023-10-17, 8:49 AM #54
Get/Set with no symbol references other than ints is definitely good enough (or at least much better than... well, nothing). In my case specifically, I tend to work A LOT with direct static.jkl references to avoid filling cogs with tons of variables for values that won't really variate ever, so that's what I'd do for SetThingPuppet and whatnot. I even use that for things that do have a symbol type like 3dos and mats as it's just better and more practical overall, although it's a bit harder for sounds as these don't have written indexes in the JKL. That way I can also keep things consistent if I change the filename of a resource without having to update every cog in which that resource is used, saves me a lot of time.

That's also my way around the fact you can't use the cog symbol type as in
cog     myCog=mycog.cog     local
as the cog symbol type references cog indexes in a JKL's cog section, so it takes int values instead. Normally the way around that would be to add custom cogs to items.dat and use GetInvCog() , but at this point I have more custom cogs than I have free slots in items.dat (also static references are just better overall for many other reasons). [/COLOR]

re.: the Scale verbs, convenience is all I had in mind really as I do a lot of SetThingRLUVecs scaling for certain visual effects like smoke, particles and stuff, and something like
ScaleThing(fx, '2 3 4');
would be A LOT more convenient than
SetThingRLUVecs(fx, VectorScale(VectorNorm(GetThingRVec(fx)),  2), VectorScale(VectorNorm(GetThingLVec(fx)),  3), VectorScale(VectorNorm(GetThingUVec(fx)),  4));

also re.: ChangeModelMaterial(), I tried it and it doesn't seem to work? Maybe I did something wrong. Also is something like
material = GetModelMaterial(model, indexOfMaterialInModelFile);
2023-10-17, 10:58 AM #55
I'll investigate ChangeModelMaterial further.

Looked into SetPOVTilt more; it's the values that govern the screen rotation when you strafe. It's incomplete as it is, where if you pass, say, the -8.0 minimum (strafing left) it then snaps you to -8.0, but if you change the -8.0 the two POVTilt verbs know about, it still snaps you to a max of -8.0 that's specified elsewhere (hardcoded).


Edit: That positive 8.0 also affects idle camera orbit speed.

Edit 2: Silver lining to digging through the POV tilt stuff is I finally decoupled the idle cam orbit speed from framerate. Like the weapon waggle, it plays by its own rules outside of the normal game logic loop. Now that I type that, I bet there are still some camera-related things that misbehave.

I dug into the SetThingMesh and SetModelMesh verbs a bit. The crash / hang happens when the game's memory management frees up the allocations for the 3D models and tries to free some data a second time (at least that's probably what I'm seeing). As far as I can tell there's no reference counting. I'm not yet sure the right way to approach this; if anyone knows how to get in touch with Xzero to see if they still have the late 2010 cogext source code...

Edit 3: Oh, some JKUP documentation says that ChangeModelMaterial doesn't work. It looks like it's trying to replace a material index in a model's material list, but it must not be a functional way to actually cause it to change.

Edit 4: Looks like I need to improve first person weapon animations and screen shake to be framerate independent next.

Edit 5: I'd been fixing the first person stuff piecemeal and in varied ways when the flaw in the vanilla game's logic finally became clearer to me (and why OpenJKDF2 didn't experience it, not due to a bug fix, but just a fundamental change to its rendering that side-stepped it), so now I have it fixed as a whole, and can describe JK's vanilla issues:

- Vanilla with stable framerate:

below 2 = game slows down, physics is pretty bad (e.g. player can't jump)

between 2 and ~5 = physics is pretty bad (e.g. player can't jump)

between ~5 and ~12 = minor physics issues (e.g. player doesn't jump full height)
between ~12 and 41. [SUB]_[/SUB]
between ~12 and 41.6 = zero issues (within reason)
between 41. [SUB]_[/SUB]
between 41.6 and 47.62 = mouse half-frame input delay and excess left/up / missed right/down

between 47.62 and 100 = first person animations, waggle, shake and idle orbit cam all get faster and faster

above 100 = game runs extra fast

- After the next version:

below 5 = game slows down, minor physics issues

between ~5 and ~12 = minor physics issues (e.g. player doesn't jump full height)

between ~12 and 1000 = zero issues (within reason)

above 1000 = game runs extra fast (but also basically impossible)

- All of that assuming no OTHER issues are found. Stuff that didn't happen in JK's main logic, which was skipped if not enough time had passed, still relied on timing variables that only updated if the logic code DID run. OpenJKDF2 avoided the issue by only having that other "stuff" run if the main logic code ran that pass, while I just gave that other "stuff" its own timing variable.

Edit 6:

Lots of high framerate fixes and some prep work to fix the cogext POVTilt verbs.
First person animations, waggle, shake, idle cam orbit speed, water distortion.

No change regarding AI turning yet; I've done a fair bit of testing with AI looking in bonkers directions and the "stop turning" code always resolves correctly... I know that COG fragment you posted was stripped down, but what's the context for VectorSet(0, 0.15, next*0.0001)? ...because if you're hoping 0.15 is the same amount of yaw when level versus when not level, it's not. I have a COG that fakes rotation in a similar way (assuming that's what you're doing):
      rad_r = (VectorX(GetThingThrust(player)) - 2.0) * 10;
#         thrust = GetThingHeadLVec(player); # buggy?
         thrust = GetThingLVec(player);
         new_z = GetThingHeadPitch(player);
         cos_r = Cosine(new_z);
         thrust = VectorSet(VectorX(thrust) * cos_r, VectorY(thrust) * cos_r, Sine(new_z));
         old_x = VectorX(thrust);
         old_y = VectorY(thrust);
         sin_r = Sine(rad_r);
         cos_r = Cosine(rad_r);
         thrust = VectorSet(cos_r * old_x - sin_r * old_y, sin_r * old_x + cos_r * old_y, VectorZ(thrust));
         id = FireProjectile(player, apos, negone, negone, nilvec, VectorSet(0.0, rad_r, 0.0), 0.0, 0x3, 0, 0);
         thrust = GetThingLVec(id);

...and just had to come to terms with the rotation speed being different (faster if I recall) if I'm looking up or down. cogext is a variable set at startup based on if cogext verbs are detected. I think those code branches produce the same result... it's been a long time since I wrote it.

But anyway, I'm wondering if the issue isn't that there's something wrong, but that vanilla when you use AISetLookPos, it forces the thing level, so it was the same turn regardless of pitch, but now that pitch survives, it's further than intended for the rest of your logic once you pitch far enough. Or in other words, that trying to calculate a turned look vector that way is mathematically incorrect and just now it matters for AISetLookPos.

Edit 7: Bah, I see now that I was looking at the wrong vector in FireProjectile; you're using the "offset_vec" while I was using the "error_vec" so the results really aren't related. Hm. I really need a reproducible test case. -_-

Aside from the stuck turning issue, how does your COG's result differ between vanilla and 2023? Do those body parts snap level vanilla and not in 2023? Do they work as you want in vanilla and not in 2023 (due to the stuck turning issue)? etc.
2023-10-19, 9:28 PM #56
Re.: framerate physics fixes
Amazing! Hopefully it will fix the issue of fast projectiles exploding mid-air in levels with 3do architecture when FPS drops.

Are GetCameraPos() and GetCameraSector() verbs feasible? (JK13 Extension includes GetCurrentCameraSector() as a verb, didn't remember that. cool). GetCameraPos() would still be nice for 3do sprites and other VFX that are generally based on player position when should be based on camera position, like rain and stuff.

Re.: AI Look direction/turning stuff
I'm certain the stuck turning stuff is in fact related to AISetLookPos() resetting pitch and roll in vanilla but not here, as it disappeared as soon as I removed SetThingPYR() from the code. I've noticed in the past that SetThingPYR() (or rather GetThingPYR() instead) produces some quite inaccurate results. One case where that was particularly noticeable was with how I use Set/GetThingPYR() to set car alignment relative to wheel positions in my car code, and if I attempted to update car orientation every frame the car would noticeably very slowly turn (even if standing still) until it reached a PYR of either '0 0 0', '0 90 0', '0 180 0' or '0 270 0'.

The offset vector thing in FireProjectile is just to get a position for the actor to look at. I'm pretty sure 0.15 was an arbitrary value, but 0.0001 was because I needed the lowest possible value that'd still cause the game to update head pitch, as head pitch is visually reset when using SetThingModel(). Otherwise, when changing, say, a weapon or armor mesh while stationary and not looking straight ahead I'd have the armor/weapon mesh floating as if they where facing forward while the player's head would be floating looking whatever way it was looking before.

Edit: if I replace SetThingPYR(dummy, GetThingPYR(player)); with SetThingRLUVecs(dummy, GetThingRVec(player), GetThingLVec(player), GetThingUVec(player)); the problem goes away. So there's probably nothing wrong with your code, but rather with SetThingPYR().

Edit 2: The problem also disappears with SetThingPYR(dummy, VectorSet(0, VectorY(GetThingPYR(player)), 0));

Edit 3: I'm confused as to what exactly you'd use that cog snippet for. What do you need the player's thrust for? I'm assuming that's to try and control the "speed" of the rotation? Also, is the code inside the cogext brackets less computationally expensive than the latter using FireProjectile, or is that more like a proof of concept? That just seems so incredibly complicated when I assume you could do something similar by just adding the LVec and RVecs multiplied by some constants and normalizing that, but maybe that vector math is actually even more expensive? (also less accurate, as you wouldn't be working directly with angles?) (it also wouldn't account for head pitch so yeah it's not a good idea)
2023-10-20, 2:31 AM #57
Sorry to burst your bubble, but low framerate physics aren't fixed. You read too much into my mention of physics. All I did was change the minimum framerate (below which the game slows down and doesn't simulate more time passing than that limit). 5 fps has fewer issues than 2 fps, but still plenty.

What does GetCameraOffset give you? It's only allowed for cameras 0 and 1.

Thanks for the further info on SetThingPYR. Looking at its code, it's fairly simplistic: takes the input vector, passes it through a vanilla function to build a rotation matrix, which then... maybe writes it straight to the target thing's RLU vectors? It does what, at a glance, looks like surprisingly little.

The most simple of related functions, at a glance, are Get/SetThingHeadPYR, as they just read and set a vector directly in thing data.

Oh, that issue because of SetThingModel was what I wasn't aware of.


Edit: Looking at MotS's SetThingLookPYR, they appear to do a fair bit more math to generate the resulting RLU vecs. Hm, though I suspect like SetThingLook, it throws out the thing's uvec and results in it snapping upright? That doesn't seem like it'd be super useful so I'm unsure if that's actually its behavior. I may set that up as an experiment for you to test against your COG(s). i.e. modify the cogext SetThingPYR to do the math that SetThingLookPYR does.

The COG snippet rotates a thing to its right, and the player's strafe thrust is used to increase (left) or decrease (right) the speed of rotation. The player is locked in place (sorta) when this piece of COG is used, so movement controls (WASD+mouse) are used to control something else. The stuff in the cogext branch is far less computationally expensive than the FireProjectile branch and as far as I recall produces the same vector, and in multiplayer it should be pretty beneficial to avoid the FireProjectile+DestroyThing calls.

FireProjectile might be the single most computationally expensive COG verb; FirstThingInView is probably also a contender.

Edit 2: I worked out a test, got SetThingPYR modified to replicate SetThingLookPYR and like I suspected, it snaps upright. GetThingPYR and MotS's GetThingLVecPYR were already the same (basically just a call to a vanilla JK matrix helper function called ExtractAngles). And regardless, it has the same "drifting" issues cogext's PYR verbs have anyway. I don't know if it's a floating point imprecision issue during the conversion to-and-or-from RLU <-> PYR, a shortcoming of the math that makes the conversion (e.g. imprecision for speed), or a mistake in the math, but I don't think it's something I can fix.

That said:
SetThingRLUVecs(dummy, GetThingRVec(player), GetThingLVec(player), GetThingUVec(player));
...should be far less computationally expensive than...
SetThingPYR(dummy, GetThingPYR(player)); other than being visually bulkier, it should be cheaper. Well, and perfectly precise. Fundamentally it's just copying 36 bytes from one place to another.

I'm assuming these things are attached to the player? You could probably also do:
TeleportThing(dummy, player);
AttachThingToThingEx(dummy, player, 0x8);
TeleportThing detaches the thing, copies the RLU vecs, position, and sector, tries to do a floor reattach if move=physics (movetype == 1) and physics flags & 0x40, and updates the camera if the teleported thing is the local player. That sounds like a lot, but if the teleported thing doesn't have physics flag 0x40 and isn't the local player, it's probably acceptably cheap. The sector move is the most expensive part, but the point is this'd be visually compact and keeps the position synchronized.

Edit 3: Okay, cogext 2023 experiment number 1:
Apply it to the JK.dll that's bundled with JK 2022 (which is identical to the JK13 JK.dll).
It should have a filesize around 768 KB and a timestamp around Dec. 22, 2010.

It fixes SetPOVTilt(vector);, SetGameSpeed(positive_float); and SetPlayerSpawnTime(int_ms);.

SetPOVTilt affects the screen tilt when strafing: the vector X is speed-to-tilt-scalar (default 5.0), Y is max left tilt (default -8.0), Z is max right tilt (default 8.0).

SetGameSpeed is a multiplier on the slowmo cheat code / DebugModeFlags 0x400 (default 0.2, can be values >= 1.0 to speed up).

SetPlayerSpawnTime I do not know, but it's an integer in milliseconds.

Edit 4: And something I expect you'll be more excited about, cogext 2023 experiment number 2:

Reimplemented HeapLoad() and HeapSave() from scratch.

- They don't take any arguments and you intentionally cannot directly specify a filename, instead the filename is the name of the COG file the verb is called from, saved to / loaded from the current player's save folder, like so:
Jedi Knight\player\QuibMask\item_keyimperial.bin (my typical surrogate test COG)
- If the COG already has called HeapNew (or loaded a heap with HeapLoad), and HeapLoad is called, it's effectively the same as calling HeapFree first. So you don't need to call HeapFree (or HeapNew) when reloading from file.
- If you want to check if a HeapLoad succeeded, use HeapSize and see if it's non-zero.
- If there's no COG heap, HeapSave will do nothing.
- If there's no heap file, HeapLoad will do nothing. It won't even remove an existing heap you were expecting to replace. Hm. I guess that makes determining state in some situations a bit ambiguous. For now, I guess I would always recommend calling HeapFree before HeapLoad and then HeapSize to check for success.
- There's no way to delete a heap file from within COG. You can basically empty one out using HeapNew(1); HeapSave(); but that seems silly.
2023-10-23, 9:17 PM #58

GetCameraOffset returns (and SetCameraOffset changes) the offsets for third person camera, but they're relative to thing position and look vector and won't give absolute values. Of course I could use these values to find the actual camera position by scaling the camera focus' look vector, but I was thinking of something that would also work for the idle rotating camera and death cameras. But of course, only if that info is as readily available as the current camera sector seems to be judging by the existence of GetCurrentCameraSector().

re.: PYR stuff
I managed to rewrite the parts of code that used Set/GetThingPYR to use R/L/U Vecs instead. The thing is that it wasn't as simple as copying the player's PYR into the body things, there's some other fancy stuff that I'm able to do by having the player model independent from the actual player thing (such as tilting sideways when turning while running and facing the direction of movement when strafe running) and it took a while to get that right without SetThingPYR. But yeah, that issue's solved.

re.: CogExt stuff
I'm assuming SpawnTime is linked to how long it takes from the moment the player dies to the moment respawn becomes available? Haven't tested, but would make sense.
The other verbs worked perfectly.

Now regarding HeapSave/HeapLoad, I'm under the impression HeapSave won't overwrite an existing file. I do not know if that's a Linux thing, but I know for sure it's not a problem with my code as it will save successfully if the save file doesn't already exist.
I came up with a super simple solution for determining state already: I made the first entry to the heap into a status flag. It is set to 0 at startup, then to 1 right before HeapSave, then to 0 again. If I run HeapLoad and find that HeapGet(0) equals 1 that means loading was successful, in which case I set that value to 2 (then again to 1 before HeapSave then back to 2 again) to indicate the game has been loaded so it won't try to load again.
2023-10-23, 9:58 PM #59
A problem with getting any camera state is it's stale; it updates after COG stuff, so any info you get is where it was on the last rendered frame (which isn't necessarily the same as the last logic frame), not where it will be this frame. It may all still be usable, but just a little warning.

My sympathies for the PYR stuff. The more I looked, the more like it seems there's no lossless conversion with what JK has to work with and strictly using RLU is the only way to avoid imprecision. Internally, JK uses RLU for the main thing orientation, and a PYR vector for the head pitch (it only uses the pitch component as far as I know), so a head-LVec is also a conversion.

Whoops, not overwriting the heap file wasn't intentional, I must've just botched something and definitely didn't test thoroughly. Wanted to get something at least modestly functional ready for you ASAP. What are your thoughts of it being linked to the COG filename it's called from (and to the player name)? Anyway, will fix that and post before I go to sleep.


Edit: This should fix the HeapSave overwrite failure.

Also snuck in tweaks to GetCameraFOV, GetCameraOffset, SetCameraFOV and SetCameraZoom to allow cameras 0-6, instead of only 0-1. Not sure if all of them will function, or if any will, but worth trying. No changes to GetCurrentCameraSector because I don't think it needed any, nor SetCameraOffset because it works differently.
2023-10-24, 8:46 PM #60
Fantastic, finally I can get rid of the memory scan hack I dreaded used for so long. That said, I think I do notice some stalling every time the HeapSave function is called for the main "server" cog (but that's probably on me for making a heap with 197633 entries - maybe I should cut that down a bit).

Anyway, thank you for providing a much safer, faster and practical alternative. Having saves based on player profiles is perfect, and pretty much exactly what I aimed to do with the memory scan based launcher hack but I could never figure out how to properly read from memory what the current player profile was.

Re.: the FOV/Zoom verbs, would it be possible to implement the smooth zooming as seen in MotS? IIRC that's what the last param of SetCameraZoom was supposed to be but it was never implemented.

Fun fact regarding PYR/RLU vecs stuff - I accidentally found out you can create a perfect mirror image of a thing by scaling its RVec by -1, and it won't turn its faces inside out as I though it would. All animations are mirrored as well. I might be taking advantage of that for making left-handed characters without having to remake every mesh and animation.
2023-10-24, 11:16 PM #61
Each heap entry is 16 bytes, so you're looking at a 3 MB file for 197633 entries. HeapSave should be a synchronous file write so 3 MB is definitely not going to be instant.

To confirm, SetCameraZoom does take 3 arguments: a camera number int, and two floats (flexes). The second float goes completely unused. I'm sure it's just modeled after the MotS verb.

Not likely on the smooth zoom without more work than I find acceptable given it should be almost trivially possible to do with COG timers (or pulse, whatever). For example:
- Save a variable of GetGameTime() (let's say initialGameTime) when you start the zoom process and set a minimum timer (1e-45) so it fires next frame.
- Use GetGameTime()-initialGameTime to get the time delta and interpolate your desired zoom level over time (in milliseconds) with SetCameraZoom.
- Repeat making new timers until the zoom time you want (in milliseconds) has passed and you've set zoom to the target amount.

In contrast, adding that capability to game logic would require hoping there's room in the camera structure (which is unnecessarily massive so there probably is) to store necessary zoom values and modify camera code so that it processes those values every render frame. The annoying part is having the cogext code patch the camera code. If I were adding COG verbs to the base executable, also patching the camera code wouldn't be a big deal. But I'm trying to do them "correctly" as cogext verbs so I can't acceptably patch the base EXE to support a feature that should be handled entirely by the cogext DLL, and the way I'm having to hack the cogext DLL to make these changes makes patching into game logic in new places a real *****.

While I now have early (2006) cogext source code, it's just reference material; it's not usable for compiling the late 2010 cogext DLL. I'm patching the DLL the same way as the EXE: with a debugger and assembler.

And because DLLs are inherently relocatable, I have to be mindful of absolute addresses because modifying the relocation table is super tedious; the first thing I did when embarking on this was to find some hijackable absolute addresses that were already in the relocation table to make a helper function for calculating absolute addresses when I need them. Thankfully SetPOVTilt had what I needed since it was oddly taking 3 ints as input, which each used a function call via an absolute address, so I changed one into a vector input and shunted the other two off for use in helper functions (one for calculating absolute addresses at run time and the other for newly added VirtualProtect to resolve the crashes when writing to read-only memory).


Edit: cogext experiment 4, some new verbs:

GetMusicVol(); returns float 0.0-1.0 : current COG-controlled music volume (SetMusicVol())
jkGetMusicVol(); returns float 0.0-1.0 : "Music Volume" slider in Sound options menu
jkGetSFXChannels(); return int 0-32? : "Digital Channels" slider in Sound options menu
jkGetLowResSound(); returns int 0 or 1 : "Using ???-Res Sounds" in Sound options menu
jkGet3DSound(); returns int 0, 1 or 2 : 0 if "Enabled A3D Sound" is unchecked, 1 if checked but A3D load failed, 2 if enabled and active
jkGetAutoAim(); returns int 0 or 1 : "Enable auto-aiming" checkbox state in Gameplay options menu
jkGetControlOptions(); returns bit field :
0x2 = "Always Run" checkbox state in Controls Options menu
0x4 = "Enable Free look" checkbox state in Controls Options menu
0x10 = "Enable automatic view centering" checkbox state in Controls Options menu
0x20 = "Disable joystick" checkbox state in Controls Joystick menu

Edit 2: JK 2023 rev 200 and cogext experiment 5:

The update to JK is to fix vanilla bugs in unused COG verbs and to add an index number to sprites so they can be used in COG (sanely).

The update to cogext adds the following unused vanilla verbs (these were just sitting there in JK.exe, unused):
GetEyePYR(thing) returns vector, same as GetThingHeadPYR but requires thing be actor/player
GetLoadedFrames(thing) returns int, only works on movetype path things
GetFramePos(thing, int) returns vector, only works on movetype path things
GetFrameRot(thing, int) returns vector, only works on movetype path things

The update to cogext also adds the following two new verbs:
GetThingSprite(thing) returns sprite index (or -1 if not sprite)
SetThingSprite(thing, sprite_index) returns previous model or sprite index (no way to know via this verb, use GetThingModel or GetThingSprite, -1 returned if wrong type)

SetThingSprite frees the thing's puppet / nulls its animclass (related but slightly different things; the pup files we usually think of as puppets are internally animclasses in spite of the thing template property name, and the puppet is the data structure that handles the 4 animations per thing) because keyframe animations crash when applied to a sprite thing. Technically the 3rd person weapon mesh does too, but lack of animclass means it won't render so should be safe there, but pretty sure a lightsaber blade will still crash if its thing is turned into a sprite.

Anyway, all of that means if you turn a 3do thing into a sprite thing, then back to a 3do thing, it will no longer have animations. You can use ParseArg(player, "puppet=ky.pup"); (or similar) to get it animating again for now. With what I learned setting up the sprite verbs, I intend to try and do the same for animclasses (which I'll call puppets in the verb names). It was way more complicated than I was expecting though; hoping animclasses won't be quite as involved.

Edit 3: JK 2023 rev 201 and cogext experiment 6:

The update to JK is to add an index number to animclasses (puppets) and soundclasses so they can be used in COG (sanely).

The update to cogext adds the following four new verbs:
GetThingPuppet(thing) returns animclass (puppet) index (or -1 if none)
SetThingPuppet(thing, animclass_index) returns previous animclass index (or -1 if none or failure), no safety so can likely cause crashes if added to things that should not try and play keyframe animations; I'll likely improve this so it only works if called on a thing that possesses a 3do model
GetThingSoundClass(thing) returns soundclass index (or -1 if none)
SetThingSoundClass(thing, soundclass_index) returns previous soundclass index (or -1 if none or failure)

If turning a 3do-having thing into a sprite, you can use GetThingPuppet first to store the thing's animclass to restore later.

Adding the index numbers to animclasses and soundclasses required reducing their max filename length from 31 down to 27. Likely this will never matter (typical filenames for these are ky.pup, ky.snd, etc.; very short), but mentioning so it's written down somewhere. There was nowhere else in their data structure to stash their index number that didn't worry me that it'd have side-effects. I might tweak this to be 29; need to double-check animclass/soundclass limits, but I doubt they're more than 32768 static + 32768 level.

Oh right, and none of these new SetThing verbs sync in multiplayer. If you want to synchronize such changes, set up trigger messages.

Edit 4: JK 2023 rev 202 and cogext experiment 7:

The update to JK reduces the space used by the animclass (puppet) and soundclass index numbers in their respective structures.

The update to cogext adds the following new verbs:
GetCogFlags(cog) returns int
SetCogFlags(cog, int) sets cog flags
ClearCogFlags(cog, int) clears cog flags
These are basically completely untested.

Also adds a safety to SetThingPuppet so it fails if the thing doesn't have a 3do model and fixes a bunch of flawed function calls (to the JK executable from the cogext DLL; a mistake on my part).

Like I suggested in edit 3, I did smush the index numbers down to two bytes for animclasses and soundclasses, so their max unique filename is back up to 29 characters.
2023-10-29, 2:43 PM #62
Fantastic! Haven't gone through everything yet, but in quick testing I noticed you can't free a thing's puppet/animclass via SetThingPuppet as you can with ParseArg "puppet=none". Maybe setting it to -1 could do that?

I'm particularly intrigued by those unused/hidden vanilla verbs, and what other functions lie unused in jk.exe wink wink Dx5 vertex fog wink wink
2023-10-29, 3:49 PM #63
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.


Edit: Okay, here's what I consider an important JK update (and also some more cogext stuff):

First, for cogext, the changes for SetThingPuppet and SetThingSoundClass to allow -1, and some new verbs:
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:
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:

Fixed (implemented, really) ChangeModelMaterial(model, index, material);
Added GetModelMaterial(model, index); returns material

jkStringConcatMaterialName(GetModelMaterial(GetThingModel(player), 0));
ChangeModelMaterial(GetThingModel(player), 0, LoadMaterial("kyfacgrn.mat"));

Edit 4: cogext 2023 experiment 10:

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:

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:

SetThingMesh(player, GetThingJointMesh(player, 0), LoadModel("st.3do"), GetModelNodeMesh(LoadModel("st.3do"), GetPuppetJointNode(LoadPuppet("st.pup"), 0)));

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:
p = GetThingPuppet(thing);
m = GetThingModel(thing);
n = GetPuppetJointNode(p, 0); // 0 = head
for(l = GetModelNodeMesh(m, n), n = GetModelNodeChild(m, n); l == -1 && n != -1; l = GetModelNodeMesh(m, n), n = GetModelNodeSibling(m, n));

And I'm not positive that'd be a valid for loop in COG.

Edit 6: JK 2023 205 and cogext 2023 12:

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
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:

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.
jkStringConcatModelName(GetThingModel(GetThingTemplate(player) | 0x80000000));

Now for cogext. First off, SetModelMesh and SetThingMesh are now functional and safe to use. SetThingMesh is the same as:
SetThingModel(thing, CloneModel(GetThingModel(thing)));
SetModelMesh(GetThingModel(thing), arg2, arg3, arg4);

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:
   m = GetThingModel(player);
   n = GetPuppetJointNode(GetThingPuppet(player), 0); // 0 = head
   for(l = GetModelNodeMesh(m, n), n = GetModelNodeChild(m, n); l == -1 && n != -1; l = GetModelNodeMesh(m, n), n = GetModelNodeSibling(m, n));
   jkStringConcatModelMeshName(m, l);
   m = LoadModel("st.3do");
   SetThingMesh(player, l, m, GetModelNodeMesh(m, GetPuppetJointNode(LoadPuppet("st.pup"), 0)));
2023-11-06, 8:15 PM #64
Okay, that's too much new stuff for me to comment bit by bit. Splendid.

Funnily enough, I knew I could check for static resources by their number, but I never thought of using bitwise operators for that, instead I was using math. I assume I'll get a ridiculously marginal performance boost for that, but it's optimization nonetheless.

I'm very happy with the new/fixed Model/Mesh verbs, they'll be incredibly useful (and allow me to break through some self-imposed resource limits on JKGR - I had some 64 copies of an empty model with ky.pup node hierarchy for use with SetModelMesh since SetThingMesh/CloneModel were not properly operational)

Now, I'm particularly intrigued by the most recent String manipulation verbs. I had never actually realized that potential, and I wonder how much more can be done?

As soon as I read the new verbs you've added I had some ideas that hopefully could be useful, but idk if feasible:

GetStringLength("string" or ÜÜ) - returns an int with the number of characters in a string
GetCharAtString("string", int position from 0 to length of string-1) - returns int?

jkHideUNIText(bool) - 0 to show text on top of screen, 1 to hide it

jkConsoleOpen(bool) - handles in-game text console, maybe 1 to open, 0 to close? maybe -1 to toggle?
jkConsoleHide(bool) - hides just the console text (if open) but not general UNI text? are these even treated differently by the engine? you know much better than I do.
jkConsoleCurInput() - returns a string with whatever's currently written in an open command console/chat
jkConsoleStatus() - retuns 0 if closed, 1 if open, 2 if hidden?

Probably not very useful or redundant:
GetUNIString(int cogstring) - returns a string variable? probably the same as using ÜÜ after jkStringClear() + jkStringConcatUniString()
GetUNIStringLength(int cogstring) - same as GetStringLength but for unistrings? maybe the same as above applies

jkConsoleLastValidInput() - returns a string with the last valid console command (eg. a cheat code)
jkConsoleLastInput() - returns a string with the last used console command, valid or not

I'm thinking the console verbs could be used for taking text input from the player, for maybe a password puzzle in a single player level or even creating custom cog controlled cheat codes or commands.

Edit: Since we're talking new verbs, I thought of some others:

DamageSurface() - same as DamageThing(), but target is a Surface
ActivateThing(sender, source) - same as activating a thing in game. sender is the thing that's being activated, source is the thing that's activating
ActivateSurface(sender, source) - same as above but for a surface

int sprite = CreateSprite(material, geoMode, lightMode, sizeX, sizeY, offX, offY, offZ) - self explanatory. Maybe similar to CloneModel in a way? Maybe offset variables could be in vector form?

GetSpriteMaterial(int sprite)
GetSpriteOffset(int sprite) - returns sprite's X/Y/Z offsets in vector form
GetSpriteSize(int sprite) - returns sprites X/Y/Z size in vector form. Z always equals 0

SetSpriteSize(int sprite, flex sizeX, flex sizeY) - self explanatory
SetSpriteMaterial(int sprite, material)
SetSpriteOffset(int sprite, vector XYZ)

FreeModel(int model) - removes a model from level/static resource list. Maybe only successful if model is cloned (and currently unused)?
FreeSprite(int sprite) - removes a sprite from level/static resource list. Maybe only successful if sprite was created via CreateSprite (and currently unused)?
2023-11-06, 10:48 PM #65
A lot of good ideas there, I especially hadn't considered accessing the player's text input.

Regarding math on resource indexes, if you were doing less than, greater than, etc. those treat the number as a float with the conversion overhead that entails.
For reference:
integer operations: && || != & | ^ !
float operations: + - * / % > < == <= >=
!= is an int op, and is why !(a == b) can give a different result than (a != b).
- as negation (as opposed to subtraction), like -1, is also a float op. It was part of why I defined an int negone=-1 symbol in most of my COGs.
The float comparison ops, while they convert their input to a float (if it's not already) before the comparison, give an integer as output, either 1 / 0 for true / false.

So, arithmetic ops, in addition to being marginally slower than bitwise ops (in isolation they'd be way slower, but most COG computing cost is in managing the COG stack machine itself, not in individual math operations, though some specific COG verbs are computationally intensive), also turn the value into a float for the next thing that uses the value, which if it wants an int, then has to convert it back. Float to int conversion is probably JK's biggest processing cost in aggregate; not like, exclusively due to COG, just in general across all its code. I slightly optimized the ftol function JK has long ago, but most cases are inlined and still dreadful.

The game has no meaningful distinction between Print and jkStringOutput text; it all gets sent to the same text buffer for showing on screen. An extra copy of Print text gets sent off to the devmode console (if it exists), but it is not sane to access.

I'll shoot down some stuff first:
- The magic ÜÜ string reference is handled at COG parse time; there's no mechanism to make string variables / references at COG runtime, and if there was, they'd blow up when you save and load. Well, or any variables at runtime; they're all made when the COG is loaded and parsed. It's why I used ÜÜ and not a verb like jkStringReference(). Sooo, stuff like jkConsoleCurInput() wouldn't be viable as is, but jkStringConcatInput() (or similar), that copy the input into the jkString, would work. It's why I added all those jkStringConcat verbs. As annoying as ÜÜ is, for vanilla it's the same as "" (an empty string) so COGs using it don't inherently blow up vanilla JK / older JKUP.
- When working on the model mesh manipulation, I did look into if freeing resources was viable, but it's not. JK's resources aren't reference counted, so any freeing would require brute force searches of anything that could be using it, so things and templates mainly for models, models and surfaces for materials, and so on, but models have gotchas (like weapon mesh and POV model). COG variables holding resource index numbers would become wrong, but at least not crashy (probably). And JK's reference lists don't accommodate gaps, so anything freed would either have to be the current end of the list, or excruciating mass data moves + index fixups would be necessary to close gaps. Uhm, and they're big-ass arrays of structures (e.g. worst I know of is animclasses that are 4160 bytes each), not like just a list of 32-bit pointers, otherwise closing the gaps wouldn't be too bad.
- Direct runtime sprite creation seems... hm, possible. But I don't think I want to tackle this any time soon, which means I dunno if I'd ever get to it. And it's because the mesh stuff was so freaking tedious; I think sprites would be far less painful, but probably still really painful. And to flesh out the idea, CloneSprite likely would be no different in difficulty than CreateSprite; all the work is in building the base object, whether the values are populated directly or copied in. What use case do you have in mind for all the sprite stuff?

On to more positive stuff:
- String length and getting character at position as integer are totally possible. I'm torn on whether I'd do it only as jkString verbs or if I'd also support string literals. Having a version of the char offset for static strings would functionally give you table lookup capability in COG without needing big wasteful symbol arrays like you're limited to currently (e.g. force pull tpl[GetCurWeapon(player)]).
- Sprite attribute verbs are probably all viable; if I do them, SetSpriteSize would take a vector as input so that SetSpriteSize(sprite1, GetSpriteSize(sprite2)); is valid. Vectors are cheaper COG variables to process than two floats. In fact, all COG stack entries are basically vectors that only use the X slot.
- I really like the DamageSurface and ActivateSurface/Thing ideas; I'll definitely look into if those are viable.


Edit: cogext 2023 rev16:

New feature: If your CPU is newer than like 2004, a faster ftol replacement is patched in. Only handles 1 out of 4 common float to int conversions JK uses (truncate, generally programmed as a cast like (int)float); the other 3 (round, floor and ceiling) still use their terrible slow implementations.

A few new verbs as suggested:
jkStringLength(); returns int
jkStringGetChar(char_index); returns int of literal wchar word value
StringLength("string"); returns int
StringGetChar("string", char_index); returns int of literal char byte value
DamageSurface(surface, damage_amount_float, damage_flags_int, thing_who_damaged); thing can be -1, returns float
DamageSector(sector, damage_amount_float, damage_flags_int, thing_who_damaged); thing can be -1, returns float
SendThingMessageEx(thing, message_type_int, param0_float, param1_float, param2_float, param3_float, thing_who_sent_message); thing_who_sent_message can be -1, returns float
SendSurfaceMessageEx(thing, message_type_int, param0_float, param1_float, param2_float, param3_float, thing_who_sent_message); thing can be -1, returns float
SendSectorMessageEx(thing, message_type_int, param0_float, param1_float, param2_float, param3_float, thing_who_sent_message); thing can be -1, returns float

The Send*MessageEx verbs can be used to send an activated message. The Damage and Message verbs all get return values like SendMessageEx. The DamageSector verb isn't obviously useful, but when setting stuff up I noticed sectors do have special handling for damaged messages like things and surfaces do, so figured I might as well.

Edit 2: JK 2023 rev 208:

Dropped the minimum framerate back down to 2 FPS from 5 FPS because... dun dun dun:

JK 2023 now has a minimum simulation speed of 47.62 FPS (1000/21), which means, if your framerate drops below 47.62, the game will simulate as many frames of logic, physics, COG execution, etc. as have passed since the last real frame.

While I used OpenJKDF2's FIXED_TIMESTEP_PHYS as a guide (which is a fixed 50 FPS simulation, even if your framerate is higher than 50), a fundamental difference is that JK 2023 allows the simulation speed to be faster than 47.62 FPS, just not slower. I mean, I think that's how OpenJKDF2's implementation works.

And why 47.62 FPS? Frametime of 21ms? Because that's the capped logic framerate of vanilla JK. So presumably the game was designed and tested with that limit in place, even if the specific number wasn't intentional (which I highly suspect it wasn't).

Vanilla logic timer is:
CurrTimeMs > PrevTimeMs + 20ms
Or in other words:
CurrTimeMs >= PrevTimeMs + 21ms
At least 21 milliseconds had to have passed to run the game logic code.

This means at 2 FPS, you can finally jump just as high as you can at a normal framerate. I THINK I got everything right, so input, etc. all works the same regardless of framerate still, but I'm exhausted and don't want to do any more testing today.

Oh, and I made the slowmo calculation come before checking for min/max frametimes, which should make it behave better at very high or low framerates and when using the new COG verb that lets you modify the slowmo multiplier.
2023-11-09, 4:29 PM #66
I'll start from the bottom with the bad news: I wondered what would happen if the reason for the framerate drops were in fact the physics simulation or COG execution? And apparently the answer is... unfortunately, FPS drops a lot more.

Regarding new string length / char verbs, and also answering what I had in mind for live sprite handling:

It's cool knowing lines of text are actually based on proper strings. However, each and every letter in this image is a 3do object, recreated every frame with CreateThingAtPos() and resized via SetThingRLUVecs(). This is probably a terribly sub-optimal way of rendering text, but I suppose if each letter were sprites the sprite limit would blow up really quickly, and I'd have a terribly hard time adjusting sizes individually for each line of text.

Sub-optimal as it is, unfortunately it also causes a much steeper framerate drop in jk.exe rev208 compared to rev207.

graphically busy setting with various lines of text showing: ~25 fps
same setting, no text showing: ~37 fps

graphically busy setting with various lines of text showing: ~15 fps
same setting, no text showing: ~35 fps

Levels with constant physics/logic calculations happening every frame are downright unplayable with rev208

Edit: I might be wrong, but I believe SetParticleMaterial(); doesn't work as intended
2023-11-09, 9:20 PM #67
Hmm, dang. The minimum simulation speed is only any good if it's rendering that's the bottleneck, not physics or COG execution (or AI processing but I doubt that's ever the bottleneck). I'll see what I can do.

Regarding text, I wonder if it'd be practical to have one thing per line of text, with a model with a bunch of equally spaced nodes with empty meshes, then mesh copy in letters for each space in the text, then reuse that model without recopying the meshes until the text it represents changes? Maybe you'd have one master model with meshes for every letter, basically a font, and copy letter meshes out of it to the ones for showing on screen? A thing per letter seems like madness, though given JK's limitations I totally understand why that's how you pulled it off.


Edt: Okay, two IPS patches in this zip, rev208b and rev208c:

rev208b only fixed time steps things, which includes all physics.
rev208c only fixed time steps COGs, which is totally pointless without the thing updates, but it'll let you test which has the bigger impact on your framerate.

Your framerate has to be below ~47.62 for anything to happen, in which case, if it's the physics / COG execution that is the main driver of the framerate being low, it'll dive even lower as it does additional passes that frame.
2023-11-10, 4:42 PM #68
Haven't tested or experimented yet, but the text idea seems doable. I believe I avoided doing that until now as the only somewhat functional mesh-related verb was SetModelMesh and thus I'd need like a different .3do file for every single line of text and that's somewhat unpractical to say the least

Now regarding physics/COG logic stuff, would it be feasible to add thing physics flags/cog flags that made it follow whatever update rate that's not the default?

Edit: Tested the two provided patches. I tried two levels which I believe take physics and COG logic to their limit, one being the WWII Eastern Front level for JKGR and another being a racetrack with 15 AI cars plus the player's.

On the WWII level (mostly saturated with things/physics with fast projectiles flying everywhere):
rev208b - framerates dropped terribly, inconsistencies with COGs with pulses that run every frame
rev208c - everything seemed normal, like in rev207. haven't directly compared framerates but I believe 207 is faster.

On the racetrack level (mostly saturated with COG logic as all car physics are handled by COG on pulses that run every frame):
rev208b - framerates dropped but not as terribly as in rev208, noticeable inconsistencies with VFX based on pulses that run every frame
rev208c - utterly unplayable, framerates dropped vertiginously as soon as AI cars started running. Car physics were completely screwed up.
2023-11-12, 7:49 PM #69
Thank you for testing those. Darn though, I guess it was to be expected that both physics and COG could be overwhelmed. While I likely will set up a hard toggle, I'm going to go back to the drawing board and see what else I can come up with.

I'm out of town until Wednesday though so nothing of substance until then. I knocked out some more cogext verbs that I might be able to remote in to a home computer to upload before then but no promises.

My most recent thingie has been to rewrite the lightsaber rendering code and I think the result is fantastic. Nothing modern like a glow, just a mathematically correct version of what vanilla tried to do, but the result looks so good compared to the screwiness of vanilla.

2023-11-13, 5:48 PM #70
I can imagine what the improved saber rendering must look like, but I can't wait to see it in action.

Meanwhile, I thought of two other verbs that could be super useful:

SetKeyRate(int thing, int animID, flex fps); - would change the speed at which a key is played, in similar manner to how certain puppet submodes have their playback rate linked to a thing's movement speed in a certain direction when 0x1 flags are set. Maybe that would require 0x1 flags being set beforehand?

GetThingBySignature(int signature); - self explanatory
2023-11-14, 10:39 AM #71

jkStringIsInputActive() returns int
SetThingRVec(thing, vector)
SetThingLVec(thing, vector)
SetThingUVec(thing, vector)
SetThingLookRotated incomplete; I ran out of time, probably does funny junk that deforms things

Typing this up on my phone is driving me nuts so anything further will have to wait.

2023-11-14, 5:55 PM #72
    if(!jkStringIsInputActive()) return;

    while(jkStringIsInputActive()) Sleep(0.001);


    // Lv 99 + Rank 9 + 100% XP

        SendMessageEx(32768, user0, player, 2, 9, 99);
        SendMessageEx(32768, user0, player, 2, 10, 9);
        SendMessageEx(32768, user0, player, 2, 13, 9065051);

Edit: Either I don't get how jkStringWildcard works or it's not working as intended. Unlike the above, this doesn't work
    // Spawn Bot
        Print("Spawn Bot");

Edit 2: idk if it's intentional or just unavoidable, but jkStringConcatInput(); will ignore anything after a space if the input console is closed. This means to get the desired result I have to change
    while(jkStringIsInputActive()) Sleep(0.001);

2023-11-15, 1:03 PM #73
Oh whoops, I bet I know what's happening with jkStringConcatInput(); when you press enter, the game's logic is probably chopping up the input string into null-terminated tokens in-place. I'll put in something to compensate.

You used the wildcard compare correctly so I must've botched something. Worth mentioning that your desired approach should be very efficient; jkStringIsInputActive() is almost as computationally cheap as a COG verb can be.

I forgot to mention that cogext rev17 should've fixed Get/SetParticleMaterial. Get didn't check if it was a particle before retrieving the material and set was simply busted.


Edit: JK 2023 rev209, cogext 2023 rev18:

New experiment for fixed logic framerate; this should be the same performance as 207 under heavy physics / COG load, but still do the enhanced logic precision if both physics and COG load is light AND rendering framerate is low (or frametime setting is high).

Also this update fixes the wildcard search and input string truncation. No saber thing yet, still polishing that.

GetThingBySignature isn't practical. The signatures aren't a reference, just a unique value so you can verify that a thing you took a reference to last frame is still the same thing this frame. It would require a brute force search through the whole possible thing list to find a match, and that's not acceptable. Try it in COG (a for loop with an incrementing thing index) comparing signatures against the one you want to find; doing the search in native code would be faster, but still terrible.

Not sure on keyframe speed yet, haven't looked at that system in depth enough to know if there are discrete variables to control speed per animation.

Edit 2: JK 2023 rev210, aka "good saber":
2023-11-17, 3:31 PM #74
Indeed, it's all functional now. It just hit me however that there's no way to differentiate whether input was closed via "esc" or via "enter", ie. if the input was in fact put in.
Performance under heavy physics/COG load is apparently the same now as it was in rev207, if there's any difference it's negligible.

re.: the saber
You somehow made the og vanilla broomstick sabers look good. Just gorgeous, really. It's really pleasant to see them not getting paper thin in the Sariss and Gorc/Pic cutscenes, and the "cut" tips look simply fantastic, really made blades look three-dimensional now.

Funny question: could break and continue keywords be fixed?
2023-11-17, 8:33 PM #75
I'll whip up something to deal with differentiating esc (or alt+tab) vs. enter.

It was the Sariss intro that has especially always bugged me. Why didn't they rig up different camera angles to avoid it just sitting there, paper thin? Arg. I basically rewrote the whole saber 3D model construction function. It's now a whopping 8 triangles, instead of just 6. One extra was to keep the saber core straight, the other extra was to have the tip match the core to avoid a potential 1 pixel seam between them due to floating point imprecision. The only time the new one looks a little funky is if you look at it perfectly end on.

So, while I was pretty sure I remembered looking into break/continue and concluding they can't be fixed, I did due diligence and dug back in to the COG syntax parser today (which is a nightmare to look at in a debugger, not because it's bad, but because it's clever). Anyway, it's more or less like I remembered: there's nothing to fix because break and continue don't actually exist. And the COG syntax parser is not really friendly to being changed after compilation anyway (lots of cascading lookup tables and jump tables). At the first stage of COG parsing, it'll pick up the keywords break and continue, but they always just result in the "default" in a subsequent switch statement.

Just use goto.
for(i = 100; i >= 0; i = i - 1)
if(i == 10) goto for_break;
if(i == 50) goto for_continue;
// other stuff

A continue label just before the }, and a break label just after the }, and the matching goto statements, produce the exact same COG bytecode that would be generated if break and continue keywords actually were implemented.

Here's a for loop and its exact match while loop (the two produce identical COG bytecode) to reiterate where the labels would be:
for(power = fullpower; power > 0; power = power - 0.025)
// stuff

power = fullpower;
while(power > 0)
// stuff
power = power - 0.025;


Edit: JK 2023 rev211, cogext 2023 rev19:

Fixed a crash in ChangeModelMaterial if the model was static and the new material was -1.

Slightly enhanced jkStringInputActive(int); 0 still stops input, -2 opens input directly to "Command:" in multiplayer, -1 opens directly to chat in multiplayer but still to command in single player, any other non-zero value is the same as pressing the chat hotkey (e.g. T) and will basically be the same as -1, but a slightly different code path. Hm, maybe I'll make -1 be chat even in single player...

Finished SetThingLookRotated(thing, lvec); unlike SetThingLook, it tries to maintain the thing's UVec. Will still drift over time, but better than nothing.

New verbs:
StackExchange() swap the top two COG stack entries
GetThingRLUVecs(thing) push all 3 RLU vectors onto the stack
ReplaceModelMaterial(model, mat_old, mat_new) replace texture on model by explicit mat reference rather than by mat index
jkStringIsInputCommand() returns int, 1 if current / most recent input was a command, 0 if chat
jkStringIsInputPending() returns int, 1 if input is active or input was closed with esc, 0 if input was closed with enter
IntAddInt(int, int) returns int, int version of + operator
IntSubInt(int, int) returns int, int version of - operator
IntMulInt(int, int) returns int, int version of * operator, does signed multiplication
IntDivInt(int, int) returns int, int version of / operator, does signed division
IntModInt(int, int) returns int, int version of % operator, does signed division
IntGtInt(int, int) returns int, int version of > operator, does signed comparison
IntLsInt(int, int) returns int, int version of < operator, does signed comparison
IntEqInt(int, int) returns int, int version of == operator
IntLeInt(int, int) returns int, int version of <= operator, does signed comparison
IntGeInt(int, int) returns int, int version of >= operator, does signed comparison
IntNeg(int) returns int, int version of negation operator (prepended -)
IntAbs(int) returns int, int version of Absolute(float), limits result to 0x7FFFFFFF

GetThingRLUVecs(thing) lets you slightly simplify RLU vec copying:
SetThingRLUVecs(thing_dst, GetThingRLUVecs(thing_src));

StackExchange was designed with variable assignment and verbs that push multiple results onto the stack in mind (only GetThingRLUVecs currently). Example:
uvec = StackExchange();
lvec = StackExchange();
rvec = StackExchange();

It also lets you defer assignment:
if(number >= 0)
result = StackExchange();
// result will be 100 or -100, depending on the if/else

StackExchange is efficient and swaps the top two stack values in-place without popping and re-pushing them.

Here's an example of the new jkStringIsInput verbs (and others):
   while(!jkStringIsInputActive()) Sleep(1e-45);
   while(jkStringIsInputActive()) Sleep(1e-45);
   if(jkStringIsInputCommand() && !jkStringIsInputPending())


         m = GetThingModel(player);
         n = GetPuppetJointNode(GetThingPuppet(player), 0); // 0 = head
         for(l = GetModelNodeMesh(m, n), n = GetModelNodeChild(m, n); IntEqInt(l, IntNeg(1)) && n != IntNeg(1); l = GetModelNodeMesh(m, n), n = GetModelNodeSibling(m, n));
         m = LoadModel("st.3do");
         SetThingMesh(player, l, m, GetModelNodeMesh(m, GetPuppetJointNode(LoadPuppet("st.pup"), 0)));

         SendThingMessageEx(player, 10, 0.0, 0x20, 0.0, 0.0, player);

While I used the new int operation verbs in that example, they're probably not meaningfully faster or slower than using the normal operators due to using 8 more bytes of bytecode and pushing/popping an extra COG stack value (for the verb call); their real purpose is to avoid precision errors when using large integers that get mangled when converted to floating point.
Like how in COG, 0x7FFFFFFF - 0x7FFFFFFE == -1 (obviously it should == 1). IntSubInt(0x7FFFFFFF, 0x7FFFFFFE) == 1.
2023-11-19, 3:58 PM #76
I do use goto with that same purpose already, was just asking out of curiosity.

The new verbs are indeed intriguing, as is the idea of a verb that returns multiple values. I had no idea something like that was possible, although I'll admit I'm yet to fully wrap my head around how to properly use StackExchange(). I'm sure I'll figure it out soon enough. The int math verbs are definitely an welcome addition as I've struggled with float imprecision often enough.

That said, I can't test any of these as the latest CogExt patch will cause the game to crash at startup. The .exe patch worked fine.
2023-11-19, 5:52 PM #77
Hm crud, you're on Linux as far as I know, but does it give you a memory address related to the crash? That may give me something to go on.

2023-11-19, 6:59 PM #78
Unhandled exception: page fault on read access to 0xdc0555e0 in 32-bit code (0xdc0555e0).
Register dump:
 CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
 EIP:dc0555e0 ESP:009ffc80 EBP:009ffd50 EFLAGS:00010216(  R- --  I   -A-P- )
 EAX:100546c0 EBX:10000000 ECX:00000000 EDX:009ffc64
 ESI:04133a68 EDI:004e0990
Stack dump:
0x009ffc80:  1005407c 009ffe2c 04133a68 0053c62c
0x009ffc90:  cccccccc cccccccc cccccccc cccccccc
0x009ffca0:  cccccccc cccccccc cccccccc cccccccc
0x009ffcb0:  cccccccc cccccccc cccccccc cccccccc
0x009ffcc0:  cccccccc cccccccc cccccccc cccccccc
0x009ffcd0:  cccccccc cccccccc cccccccc cccccccc
=>0 0xdc0555e0 (0x009ffd50)
  1 0x10054b8b in jk (+0x54b8b) (0x009ffe2c)
0xdc0555e0: addb	%al,0x0(%eax)
Module	Address			Debug info	Name (119 modules)
PE	00370000-00385000	Deferred        portable
PE	00390000-003ab000	Deferred        winmm
PE	003b0000-003c6000	Deferred        smackw32
PE	00400000-008f4000	Export          jk23
PE	00d00000-01ed5000	Deferred        wined3d
PE	01ee0000-022cf000	Deferred        ole32
PE	022d0000-026a7000	Deferred        comctl32
PE	038b0000-03a15000	Deferred        winmm
PE	04be0000-04bf2000	Deferred        a3d
PE	04e00000-04e97000	Deferred        mmdevapi
PE	04ea0000-04f12000	Deferred        winepulse
PE	10000000-1010a000	Export          jk
PE	5e080000-5e0bb000	Deferred        dplayx
PE	62500000-6287c000	Deferred        oleaut32
ELF	63431000-64a80000	Deferred
PE	64a80000-64acc000	Deferred        win32u
PE	65480000-65662000	Deferred        rpcrt4
PE	65680000-65875000	Deferred        msvcrt
PE	66080000-660d5000	Deferred        msacm32
PE	66640000-6665a000	Deferred        version
PE	67500000-67544000	Deferred        imm32
PE	67c00000-67d83000	Deferred        dsound
PE	684c0000-685f0000	Deferred        combase
PE	68880000-68cfa000	Deferred        user32
PE	69840000-6990e000	Deferred        advapi32
PE	69ec0000-69ec6000	Deferred        ddraw
PE	6aac0000-6ad06000	Deferred        ucrtbase
PE	6aec0000-6b060000	Deferred        setupapi
PE	6b3c0000-6b405000	Deferred        libvorbisfile-3
PE	6bb40000-6bbbd000	Deferred        winex11
PE	6bbc0000-6bc4b000	Deferred        sechost
PE	6da80000-6dc21000	Deferred        gdi32
PE	6de80000-6df20000	Deferred        dinput
PE	71080000-710a4000	Deferred        hid
ELF	746f7000-78bec000	Deferred
PE	7a800000-7ab6c000	Deferred        opengl32
PE	7b000000-7b479000	Deferred        kernelbase
PE	7b600000-7b72c000	Deferred        kernel32
PE	7bc00000-7be4c000	Deferred        ntdll
ELF	7c321000-7c32b000	Deferred
ELF	7c32b000-7c339000	Deferred
ELF	7c339000-7c356000	Deferred
ELF	7c356000-7c37e000	Deferred
ELF	7c37e000-7c3f5000	Deferred
ELF	7d000000-7d004000	Deferred        <wine-loader>
ELF	7d006000-7d016000	Deferred
ELF	7d016000-7d01a000	Deferred
ELF	7d01a000-7d055000	Deferred
ELF	7d055000-7d0d0000	Deferred
ELF	7d0d0000-7d13c000	Deferred
ELF	7d13c000-7d1bb000	Deferred
ELF	7d1bb000-7d1d0000	Deferred
ELF	7d1e2000-7d200000	Deferred
ELF	7d404000-7d408000	Deferred
ELF	7d408000-7d40e000	Deferred
ELF	7d804000-7d80c000	Deferred
ELF	7d81a000-7d82e000	Deferred
ELF	7d82e000-7d849000	Deferred
ELF	7d849000-7d857000	Deferred
ELF	7d857000-7d861000	Deferred
ELF	7d864000-7d868000	Deferred
ELF	7d868000-7d88c000	Deferred
ELF	7d88c000-7d8c3000	Deferred
ELF	7d8c3000-7d8cc000	Deferred
ELF	7d8cc000-7d8d3000	Deferred
ELF	7d8d3000-7d8fd000	Deferred
ELF	7db68000-7db6b000	Deferred
ELF	7db6b000-7db70000	Deferred
ELF	7dca9000-7dcc1000	Deferred
ELF	7dcc1000-7dcd9000	Deferred
ELF	7dcd9000-7dd65000	Deferred
ELF	7dd65000-7dd90000	Deferred
ELF	7dd90000-7dd99000	Deferred
ELF	7dd99000-7ddfb000	Deferred
ELF	7ddfb000-7dedc000	Deferred
ELF	7dedc000-7def2000	Deferred
ELF	7def2000-7df1d000	Deferred
ELF	7df1d000-7df2f000	Deferred
ELF	7df2f000-7dfbe000	Deferred
ELF	7dfbe000-7dfc9000	Deferred
ELF	7dfc9000-7e059000	Deferred
ELF	7e059000-7e0b4000	Deferred
ELF	7e0b4000-7e141000	Deferred
ELF	7e141000-7e19b000	Deferred
ELF	7e257000-7e263000	Deferred
ELF	7e26b000-7e272000	Deferred
ELF	7e272000-7e285000	Deferred
ELF	7e3fe000-7e45d000	Deferred
ELF	7e45d000-7e470000	Deferred
ELF	7e470000-7e474000	Deferred
ELF	7e474000-7e481000	Deferred
ELF	7e481000-7e48d000	Deferred
ELF	7e48d000-7e494000	Deferred
ELF	7e494000-7e498000	Deferred
ELF	7e498000-7e4a2000	Deferred
ELF	7e4a2000-7e4bd000	Deferred
ELF	7e4bd000-7e4c4000	Deferred
ELF	7e4c4000-7e4c9000	Deferred
ELF	7e4c9000-7e4f5000	Deferred
ELF	7e4f5000-7e63f000	Deferred
ELF	7e63f000-7e654000	Deferred
ELF	7e655000-7e658000	Deferred
ELF	7e674000-7e67b000	Deferred
ELF	7e67b000-7e687000	Deferred
ELF	7e687000-7e711000	Deferred
ELF	7e711000-7e7b1000	Deferred
ELF	7e83d000-7e86f000	Deferred
ELF	7e86f000-7e8ba000	Deferred
ELF	7e8ba000-7e8d9000	Deferred
ELF	7e8d9000-7e913000	Deferred
ELF	7e913000-7e9d0000	Deferred
ELF	7e9d0000-7ead2000	Deferred
ELF	7eb05000-7ec78000	Deferred
ELF	7ec78000-7ee00000	Dwarf 
ELF	f7cbc000-f7d73000	Deferred
ELF	f7d73000-f7f4f000	Deferred
ELF	f7f4f000-f7f54000	Deferred
ELF	f7f54000-f7f74000	Deferred
ELF	f7fa9000-f7fd1000	Deferred
process  tid      prio    name (all IDs are in hex)
00000038 services.exe
	0000003c    0     
	00000040    0     wine_rpcrt4_server
	00000054    0     wine_rpcrt4_io
	0000007c    0     wine_rpcrt4_io
	00000090    0     wine_rpcrt4_io
	000000a4    0     wine_rpcrt4_io
	000000cc    0     wine_rpcrt4_io
	000000d8    0     wine_rpcrt4_io
	00000108    0     wine_rpcrt4_io
	00000128    0     wine_rpcrt4_io
00000044 AppleMobileDeviceService.exe
	00000048    0     
	0000005c    0     
	00000064    0     
	0000006c    0     
	00000078    0     
	000000c4    0     
	000000c8    0     
	0000014c    0     
00000070 mDNSResponder.exe
	00000074    0     
	00000080    0     
	00000084    0     wine_sechost_service
00000088 svchost.exe
	0000008c    0     
	00000094    0     
	00000098    0     wine_sechost_service
0000009c winedevice.exe
	000000a0    0     
	000000a8    0     
	000000ac    0     wine_sechost_service
	000000b0    0     
	000000b4    0     
	000000b8    0     
	0000011c    0     
000000bc winedevice.exe
	000000c0    0     
	000000d0    0     
	000000d4    0     wine_sechost_service
	000000dc    0     
	000000e0    0     
	000000e4    0     
	000000f4    0     
	000000f8    0     
000000e8 plugplay.exe
	000000ec    0     
	00000110    0     
	00000114    0     wine_sechost_service
	00000118    0     wine_rpcrt4_server
00000120 rpcss.exe
	00000124    0     
	0000012c    0     
	00000130    0     wine_sechost_service
	00000134    0     wine_rpcrt4_server
	00000138    0     wine_rpcrt4_server
	000001e0    0     wine_rpcrt4_io
000001c4 (D) E:\JK\jk23.exe
	000001c8    0 <== 
	000001e4    0     
	000001f0   15     
	000001f4    0     
	000001f8   15     winepulse_mainloop
	000001fc   15     winepulse_timer_loop
	00000200   15     wine_dsound_mixer
	00000214    0     
000001cc explorer.exe
	000001d0    0     
	000001d4    0     
	000001d8    0     
	000001dc    0     wine_rpcrt4_server
0000020c conhost.exe
	00000210    0     
System information:
    Wine build: wine-8.0.1
    Platform: i386 (WOW64)
    Version: Windows 7
    Host system: Linux
    Host version: 5.4.0-150-generic
2023-11-19, 7:44 PM #79
That was enough info. You applied the cogext IPS patch to an already patched DLL. You need to apply it to a fresh JK13 / JK2022 cogext JK.dll file each time to avoid issues like this.

In this specific case the byte at offset 0xFA06 in JK.dll came out wrong. The result was a relative jump to 0xDC0555E0 instead of 0x100555E0. Other bytes are wrong too but probably wouldn't cause a crash.

2023-11-19, 8:25 PM #80
Indeed, that was all but a rather silly little mistake on my part :colbert: I applied the patch to JK.dll when I should've applied it to JK-og.dll

All good now.

Edit: there's a tiny thing that's bugging me just a bit. Why IntAddInt() and the such instead of just IntAdd()? I mean, the vector sum verb is called VectorAdd() not VectorAddVector()

Also, have you fixed GetThingHeadLVec()?

Edit 2: I'm trying to get the hang of working without variables, I can't believe this is actual valid code but it actually makes perfect sense when I think of it
VectorScale(GetThingRVec(source), VectorDot(GetThingVel(sender), GetThingRVec(sender)));
VectorScale(GetThingLVec(source), VectorDot(GetThingVel(sender), GetThingLVec(sender)));
VectorScale(GetThingUVec(source), VectorDot(GetThingVel(sender), GetThingUVec(sender)));

SetThingVel(source, VectorAdd(VectorAdd()));

↑ Up to the top!