Welcome to Open Carnage

A resource for Halo Custom Edition and MCC modding, with unique means of rewarding content creation and support. Have a wander to see why we're worth the time! - EST. 2012

PiRate

Member
  • Content count

    11
  • Joined

  • Last visited

  • Raffle Tickets

    0

1 Follower

About PiRate

Extra Information

  • Gender
    Female
  1. This post is one part plug for a library I am writing and one part conveying a particular danger that arises when moving objects by the traditional method. If you've ever written a SAPP script that teleports an object, you may have written something to the effect of write_vector3d(obj + 0x5C, x, y, z) The danger of this method is captured in the video below. (This server was running with vehicle_incremental_rate set to 1, to better present what the server sees.) If you've ever visited the A§H clan servers, you may have noticed some vehicles bugging out upon joining, as they run a vehicle blocker script on a few of their servers that teleports vehicles in this way. Hopefully, by the end of this post, you will understand why this method of teleporting objects results in this problem and how to alleviate the issue. WHY DOES IT HAPPEN Given a possible interaction (explosion, projectile, player pressing the action key, etc) with objects (in some cases, object markers), Halo will check a set of clusters relevant to the action as a means of reducing the number of objects checked. In order for an action A to interact with an object O, it must meet at least the following: Object O must belong to at least one cluster that also belongs to the set of clusters relevant to action A, and Object O (or relevant markers) must be within a certain Euclidean distance of the action. When one teleports objects using the code snippet above, the cluster structures do not get updated automatically nor on an update pretick. The object's cluster references may be updated on the tick itself, but the video above demonstrates this will not always happen. In the video, the grenades thrown while inside the base do not interact with the vehicle because, although they are in the correct cluster, the grenade is simply too far away from the vehicle. Furthermore, the grenades thrown where it is teleported to do not interact with the vehicle because, although they are close enough, the grenades are not in the correct cluster. As a result, this vehicle is effectively in limbo until a map reset, because nothing can interact with it. It should be mentioned that the above snippet is *OK* to use for player bipeds, as the cluster-object bi-map seems to get updated each tick for them. HOW TO ALLEVIATE (FOR VEHICLES) One can force the cluster-object references to update for a vehicle by giving it a bit of momentum (e.g. give it a little speed in the x, y, or z direction) and clearing the physics deactivation bit. I cannot guarantee that this will work all the time nor can I comment on its side-effects, but at the very least it will help prevent vehicles from being stuck in limbo. write_vector3d(vehicle + 0x5C, x, y, z) -- Update position write_vector3d(vehicle + 0x68, 0.0, 0.0, -0.015) -- A little -z momentum write_bit(vehicle + 0x10, 5, 0) -- Activates physics for the vehicle If this method does not satisfy you and you do not mind adding a few dependencies to your scripts just to teleport vehicles safely, read on. HOW TO SOLVE When Halo teleports or reorients objects, it performs roughly the following steps: disconnect the object from the map, by detaching the object from its parent and clearing its cluster references; write to the position and/or direction vectors; recalculate the object's node data, which may require reading from (possibly compressed) animation tag data; connect the object to the map, by reinserting the appropriate cluster references for its new position. Steps 3 and 4 are quite lengthy to describe in detail, but I have provided a module you can use with SAPP as part of hlm that provides the functions corresponding to steps 1, 3, and 4 for your SAPP scripts. Additionally, hlm itself provides a function that wraps these steps, so you can just write hlm.object_reposition(obj, {x, y, z}) from within your scripts. The video below uses this function to safely teleport the vehicle: That being said, if you're only concerned about teleporting vehicles safely, I would recommend the alleviation method unless you need to reposition an object in parity with Halo.
  2. For the technical details of camera_track, see the spoiler at the end of this post. Library: hlm.lua Sample (shown below): Vision Test.lua This library provides SAPP script writers with the ability to accurately determine a player's camera position and aiming directions (forward, left, up), using only SAPP memory functions and lua, up to and excluding camera interpolation, third-person camera hacks, and Halo script/console commands that control the camera. Practical applications include more accurate visibility and intersection testing from the camera and a boost command that works properly from vehicles (this is left as an exercise to the reader). Many of the functions provided by the library (hlm.lua) are reverse-engineered from the client, with the intent of preserving accuracy to the client. (There is a bit of a hiccup in the video at 0:38, my apologies for this). A sample script from my repository is attached (Vision Test.lua). If you intend to use this library, please read the instructions on setting it up. The sample script should get you up to speed on usage if you plan on using the library as pitched, although there are a number of related functions you may desire to take a look at, such as get_object_markers and get_node_transform. It should be noted that in the video above, the camera position as calculated is not clipped against the terrain nor certain collideables. This behavior is faithful to how Halo calculates the position of the camera for firing purposes. Technical details on the camera_track tag:
  3. Does this also fix the BSP leaks?
  4. I had done a silly thing many years back on Custom Edition when I was familiarizing myself with scripting. I decided to try my hand at it again. Also vaguely related:
  5. Found out the reason, see bottom of post. Does Chimera cache the controls folder? I made some updates to a plugin I placed in there and whenever I run Halo with the Chimera strings.dll, it loads the stale version of that plugin that first loaded with the new version of Chimera. Even when I delete my plugin from the controls folder, the plugin still loads :spooky:. Using vanilla Strings.dll fixes this behaviour; the new version of the plugin is used or, if deleted, it is never loaded at all. I understand my plugin is not likely to be compatible with Chimera, but I have been making a number of edits to my plugin to attempt compatibility. EDIT Nevermind, found out I still had the mods folder present with a stale copy.
  6. Sound warning. For a brighter but less custom experience:
  7. Try changing the resolution down to the lowest and then back to your original resolution. I notice that when I use windowed mode the game will cap to my refresh rate despite the setting reading otherwise and it takes some messing with the video setting to make it work right again. I almost exclusively use fullscreen for this reason.
  8. For the last few weeks I've been messing around with Halo's scripting engine to the end of integrating new functions into it. Below is a working snippet of code (C++). long chat_say(std::optional<std::string_view> text) { sentutil::chat::send_chat(text.value_or("Hello, World!").data()); return 314l; } bool Load() { // Returns types and arguments are deduced by install_script_function. // The parsing and evaluation coroutine interfaces are instantiated as necessary. // Furthermore, install_script_function is not limited to function pointers, // as one can pass it a std::function (and a std::function-wrapped lambda). return sentutil::script::install_script_function<"chat_say">( chat_say, "sends chat over the global channel", // help text (usage) "[<string: message>]" // help text (parameters) ); } The installed function can be accessed, queried through help, and auto-completed like any other in Halo, and this function can even be used as follows (with devmode): begin (sleep (chat_say "ping")) (chat_say "pong") The first call to chat_say returns 314 to sleep, which causes the thread of evaluation to immediately commit ping to all chat and roughly 10 seconds later (314 ticks to be more precise) commits pong to all chat. This is not exactly a motivating example and I have no need of a framework this robust, but I suppose I learned quite a bit along the way.
  9. I am unsure of the progress on this script but I'll share some short research into it in case someone with the time and familiarity with lua can make use of it. cheat_jetpack cannot be used as it applies globally to all player-controlled units. Furthermore, marking a player as invulnerable would have undesired effects. A hack to Halo's code must be used, so that players can be selectively marked as invulnerably to falling damage. The relevant section when Halo's loads the global cheat_jetpack can be found below: CPU Disasm Address Hex dump Command Comments 0057332F |> \A0 ???????? MOV AL,BYTE PTR DS:[globals::cheat_jetpack] 00573334 |. 83CB FF OR EBX,FFFFFFFF 00573337 |. 84C0 TEST AL,AL 00573339 |. 74 0C JZ SHORT 00573347 0057333B |. 399E 1802000 CMP DWORD PTR DS:[ESI+218],EBX ; cmp unit->controlling_player, -1 00573341 |. 0F85 2D01000 JNE 00573474 ; take jump to skip falling damage check The reason the controlling player is tested against is that cheat_jetpack applies only to player-controlled units. EAX is clobbered without being read from regardless of the path through this snippet, which gives us a bit of freedom. I opt to repurpose the object flags (a DWORD) at offset 0x10 from the player's object address (specifically the most-significant bit, but I believe the 4 most significant bits go unused). CPU Disasm Address Hex dump Command Comments 0057332F \83CB FF OR EBX,FFFFFFFF 00573332 F746 10 00000080 TEST DWORD PTR DS:[ESI+10],80000000 ; unit->object_flags & 0x80000000 (most-significant bit) 00573339 |. 74 0C JZ SHORT 00573347 0057333B |. 399E 18020000 CMP DWORD PTR DS:[ESI+218],EBX ; cmp unit->controlling_player, -1 00573341 |. 0F85 2D010000 JNE 00573474 ; take jump to skip falling damage check This effectively disables cheat_jetpack, but replaces it with the ability to re-enable it on specific units by setting the most-significant bit in the object flags. The signature A0????????83CBFF84C0740C399E18020000 is sufficient to search for this snippet. This script should be a start if someone with the time wants to complete this request. I have not done ANY testing for it outside of manually writing the patch and setting the repurposed flag through OllyDBG, and I am not familiar with lua nor its integration into SAPP, so quite a bit of additional work and verification may be needed. Also note that if the player dies, they will no longer be marked as invulnerable to fall damage. This might be a blessing for this request but it is inconsistent with the original behavior of cheat_jetpack. If someone wishes to complete the script to the satisfaction of the requestor, I do not mind if they wish to take full credit for it. My apologies if my naming convention clashes with the standard convention for lua/SAPP scripting. Edit: Addresses above are from the 1.10 haloded.exe included with SAPP, but the signature works on the Custom Edition server (in addition to the 1.10 PC/CE clients). Edit2: Script linked isnt functional, will try to fix now that I have time to and SAPP set up Edit3: Script loads and unloads, properly patches, and the set_player_jetpack function works as expected now The script link above has been changed to a functional version (loads, unloads, properly patches). That being said, everything from this post still applies; the script does not complete the request, but if someone has the time and ability, they can use the set_player_jetpack function within to finish the request. The set_player_jetpack function was tested via the lua_call command. Link to the update script in this post for good measure: pastebin.
  10. I am unsure how to explain your experience with sleep_until's second parameter. Preliminary testing indicates that omitting the second parameter and setting it to 30 yield the same result. Additionally, evaluating (sleep_until false 1 90) produces 90 evaluations of the condition in 3 seconds, as expected. Investigation into the code also corroborates this. CPU Disasm Address Command Comments 004898F3 |. MOV EAX,DWORD PTR SS:[LOCAL.2] 004898F7 |. MOV BYTE PTR DS:[EAX],0 ; condition value, default 0 (false) 004898FA |. MOV EAX,DWORD PTR DS:[Globals::lpGameTimeGlobals] 004898FF |. MOV EAX,DWORD PTR DS:[EAX+0C] 00489902 |. MOV DWORD PTR DS:[EDX],EAX ; sets call_time = lpGameTimeGlobals->game_ticks 00489904 |. MOV EAX,DWORD PTR SS:[LOCAL.3] 00489908 |. MOV WORD PTR DS:[EDI],0 ; bool that is set when 2nd and 3rd args are evaluated/defaulted 0048990D |. MOV WORD PTR DS:[EBX],1E ; period, default 0x1E (30 decimal) ticks 00489912 |. MOV DWORD PTR DS:[EAX],-1 ; timeout, default -1 This snippet sets up the period to the value to 30, later taking on the value of the second argument when and if it is evaluated, and remains unchanged if the second argument is omitted. As to how that argument is used: CPU Disasm Address Command Comments 004899BF |. MOV EAX,DWORD PTR SS:[LOCAL.1] ; EAX <- &period 004899C3 |. MOV AX,WORD PTR DS:[EAX] ; AX <- period 004899C6 |. CMP AX,1 ; AX <- max(period, (short)1) 004899CA |. JGE SHORT 004899D3 ; ^ ^ 004899CC |. MOV EAX,1 ; ^ ^ 004899D1 |. JMP SHORT 004899D6 ; ^ ^ 004899D3 |> MOVSX EAX,AX ; (long)max(period, (short)1) 004899D6 |> MOV ECX,DWORD PTR DS:[Globals::lpGameTimeGlobals] 004899DC |. MOV EDX,DWORD PTR DS:[ECX+0C] ; lpGameTimeGlobals->game_ticks 004899DF |. MOV ECX,DWORD PTR SS:[LOCAL.3] 004899E3 |. ADD EAX,EDX ; EAX <- game_ticks + max(period, (short)1) 004899E5 |. MOV DWORD PTR SS:[EBP+8],EAX ; lpThread->wake_tick = game_ticks + max(period, (short)1) sleep_until's mechanism is implemented by setting a value that indicates to the script engine on what tick the script thread can be updated again, as shown here in the thread update function: Address Command Comments 0048A410 |> /MOV ECX,DWORD PTR DS:[ESI+8] ; ECX <- lpScriptThread->wake_tick 0048A413 |. |TEST ECX,ECX 0048A415 |. |JL 0048A4FC ; stops thread update if lpThread->wake_tick < 0 0048A41B |. |MOV EAX,DWORD PTR DS:[Globals::lpGameTimeGlobals] 0048A420 |. |CMP BYTE PTR DS:[EAX],0 0048A423 |. |JE SHORT 0048A43C ; skips time check if game is not ready 0048A425 |. |MOV DL,BYTE PTR DS:[EAX+1] 0048A428 |. |TEST DL,DL 0048A42A |. |JNZ SHORT 0048A433 ; skips time check if game is not running 0048A42C |. |MOV DL,BYTE PTR DS:[EAX+2] 0048A42F |. |TEST DL,DL 0048A431 |. |JZ SHORT 0048A43C ; skips time check if game is paused 0048A433 |> |CMP ECX,DWORD PTR DS:[EAX+0C] ; cmp lpThread->wake_tick, lpGameTimeGlobals->game_ticks 0048A436 |. |JG 0048A4FC ; stops thread update if lpThread->wake_tick > lpGameTimeGlobals->game_ticks Then, upon waking, the thread will check the condition again. If the condition evaluates to false, the thread is set to sleep another period ticks as indicated in the second snippet. Otherwise, if the condition evaluates to true, the script thread returns from the sleep_until call.
  11. While doing some reversing of Halo to fully integrate client functions into the scripting engine, I came across a curiosity regarding sleep_until. According to the references I can locate, sleep_until can accept two arguments - one required and one optional. Even the internal reference generated by script_doc affirms this: However, sleep_until has 3 parameters, and technically accepts even more arguments. Note: Any addresses below are for the 1.10 PC client, because most of my notes are on the PC client and have yet to be ported to the CE client. However, what I mention below holds true for the CE client. I am posting this under the CE section because I assume more people would be using Halo script in CE rather than PC. Below is the parsing function for sleep_until, in Cish (i.e. no guarantees it is legal C), translated from the assembly: // Note: I use BOOL below to convey meaning and it does not necessarily // reflect the width of Halo bools, depending on the compiler. // Returns TRUE if the script node referred to by node_id can evaluate // to the value type specified, and FALSE otherwise. // Has side-effects. BOOL parse_script_node_expected(identity_type node_id, enum_short type); BOOL parse_sleep_until(short function_index, identity_type script_node_id) { script_node *primary_node = get_script_node(script_node_id); script_node *function_node = get_script_node(primary_node->value.id); // ------------------------------ // condition argument, required // type: boolean (enum value 5) identity_type condition_node_id = function_node->next_script_node_id; if (condition_node_id == k_invalid_identity || !parse_script_node_expected(condition_node_id, value_type_boolean)) return FALSE; // condition argument is required // ------------------------------ // period argument, optional // type: short (enum value 7) script_node *condition_node = get_script_node(condition_node_id); identity_type period_node_id = condition_node->next_script_node_id; if (period_node_id == k_invalid_identity) return TRUE; // period argument is optional if (!parse_script_node_expected(period_node_id, value_type_short)) return FALSE; // period argument supplied but could not parse to short // ------------------------------ // timeout argument, optional // type: long (enum value 8) script_node *period_node = get_script_node(period_node_id); identity_type timeout_node_id = period_node->next_script_node_id; if (timeout_node_id != k_invalid_identity && !parse_script_node_expected(timeout_node_id, value_type_long)) return FALSE; // timeout argument supplied but could not parse to long return TRUE; } You can probably guess what this third argument and its type is. To confirm its meaning, here is a snippet from sleep_until's evaluation function: CPU Disasm Address Hex dump Command Comments 00489980 |. 8B00 MOV EAX,DWORD PTR DS:[EAX] ; EAX <- timeout (3rd argument) 00489982 |. 83F8 FF CMP EAX,-1 00489985 |. 74 0E JE SHORT 00489995 ; never timeout! 00489987 |. 8B12 MOV EDX,DWORD PTR DS:[EDX] ; EDX <- time of function call in ticks 00489989 |. 03D0 ADD EDX,EAX ; EDX <- timeout + function_call_time 0048998B |. A1 6C1D6F00 MOV EAX,DWORD PTR DS:[Globals::lpGameTimeGlobals] 00489990 |. 3950 0C CMP DWORD PTR DS:[EAX+0C],EDX ; lpGameTimeGlobals->game_ticks 00489993 |. 7D 77 JGE SHORT 00489A0C ; jumps to exit sleep_until context if game_ticks >= (timeout + function_call_time) The third argument is a timeout, in ticks relative to the time when sleep_until would be called! An updated description for the function should resemble: However, because Halo does not verify the number of arguments does not exceed 3, nor their type, Halo will gladly parse, compile, and evaluate the following: (sleep_until false 15 3600 "foo" 3.14159265359) Fake edit for those that read this post: you can paste text into the client console by pressing F6. I have no idea if this holds for Mac users, but I found this information out while completing documentation of the console line buffer structure. I am also unsure if this works while using HAC2. Real edit: Turns out the timeout is not in absolute ticks, will slowly edit my post to reflect this (done).