dreadedentity 278 Posted April 17, 2022 (edited) I have made several changes from the original post. You will always be able to get the latest updates from Github. The post below will still serve as a learning tool, but if you intend on using this system in your missions, you should get the updated version Hello! I am back again with something pretty fun today. First, the setup. I have been playing a lot of Arma lately and, naturally, with how involved I was in scripting and this forum years ago I eventually found my way back here. So I've been playing various scenarios that the community has made and, while generally really good, something that always bothers me is the lack of a revive system, or a bad one. So let's make one; and give it away to the community as a nice base to work with. The goal here is simple; provide an easy, low-code, feature-rich system that can be customized and/or expanded upon. Simply put, if the code wasn't required, it's not here(with some tolerance to fun stuff that I actually did add anyway). For those of you that like to "try before you buy" so to speak, I have recorded and uploaded a video to show you what you're about to be getting into: Stop. Here is your only warning: This ain't your grandaddy's forum post. It's definitely the longest I've ever posted here, possibly the longest forum post I've ever made, on top of that, I'd even expect some of the highly-knowledgeable among us to sit with this for some time and digest it. So go refill your drink, take those last bites of dinner, and buckle in Initially I was going to just do a code snippet, like I used to do way back when, with BIS_fnc_holdActionAdd, a command that I had not known about before and might not have known about were it not for my bad habit of trying to answer every question on this forum. It has a really nice effect in my opinion, and is featured heavily in the "Old Man" scenario. Anyway, I should begin To start, we have 2 options for the actual reviving part of the revive system, which I call a "holding" type or a "click-and-wait" type. I am a much bigger fan of the "holding" revive, so I will quickly go over the "click-and-wait" type so we can move on. This might be obvious, but this type uses a basic addAction for functionality: Spoiler addPlayerClickRevive = { _this addAction [format ["Revive %1 %2", [toUpper ((rank _this) select [0,1]), toLower ((rank _this) select [1, count (rank _this) - 1])] joinString "", (name _this splitString " ") # ((count (name _this splitString " ")) - 1)], { params ["_target", "_caller", "_actionId", "_arguments"]; _caller playMove "AinvPknlMstpSnonWnonDnon_medic1"; _caller playMove "AinvPknlMstpSnonWnonDnon_medic2"; _caller playMove "AinvPknlMstpSnonWnonDnon_medicEnd"; sleep 10; [_target, false] call unitSetReviveState; _target setDamage 0; }, nil, 1000, false, true, "", "lifeState _target == 'INCAPACITATED' && _this == player"]; }; To use this, just pass the unit via call or spawn. For example, let's give all players in a particular mission the ability to revive each other: { _x call addPlayerClickRevive } forEach allPlayers; Don't worry about unitSetReviveState, because I'm going to go over that in just a bit. But look, our "click-and-wait" revive system is done! Letsmoveonplease Next up, the pièce de résistance, the "holding" type revive. I like this one much more because of the effect. Either way, we're starting the same, create a function that will just add a revive action to units: Spoiler addPlayerHoldRevive = { [_this, format ["revive %1 %2", [toUpper ((rank _this) select [0,1]), toLower ((rank _this) select [1, count (rank _this) - 1])] joinString "", (name _this splitString " ") # ((count (name _this splitString " ")) - 1)], "a3\ui_f\data\igui\cfg\holdactions\holdaction_revivemedic_ca.paa", "a3\ui_f\data\igui\cfg\holdactions\holdaction_revive_ca.paa", "lifeState _target == 'INCAPACITATED' && _this == player && {_target != player && {_target distance _this < 3}}", "true", { (_this # 1) playMove "AinvPknlMstpSnonWnonDnon_medic1"; (_this # 1) playMove "AinvPknlMstpSnonWnonDnon_medic2"; (_this # 1) playMove "AinvPknlMstpSnonWnonDnon_medic3"; }, nil, { [_this # 0, false] call unitSetReviveState; (_this # 0) setDamage 0; (_this # 1) playMoveNow "AinvPknlMstpSnonWnonDnon_medicEnd"; }, { (_this # 1) playMoveNow "AinvPknlMstpSnonWnonDnon_medicEnd" }, nil, 14, 1000, true, true, true] call BIS_fnc_holdActionAdd; }; And look, there's that unitSetReviveState again. I guess I should post that before anyone gets their pitchforks out. It is just a wrapper for a few commands that give the effect of a unit being dead because I got tired of typing the same commands over and over: Spoiler unitSetReviveState = { params ["_unit", "_revive"]; if (_revive) then { _unit setUnconscious true; _unit setCaptive true; _unit allowDamage false; } else { _unit setUnconscious false; _unit setCaptive false; _unit allowDamage true; }; }; Now, those eagle-eyed among you probably noticed that none of this works unless the unit is in a particular lifeState. But how do you get into the "incapacitated" life state? Whenever I shoot something it just dies?? Well, ironically because this post gained some popularity recently, we are going to use the exact same thing, the "HandleDamage" event handler. By the way, I started working on this just days before that post got necro'd 😉. I'll let you read the documentation and the previously mentioned post on your own time, so for now just trust me that this does work: Spoiler addRevive = { _this addEventHandler ["HandleDamage", { params ["_unit", "_selection", "_damage", "_source", "_projectile", "_hitIndex", "_instigator", "_hitPoint"]; if (_damage >= 0.99) then { if !(lifeState _unit == "INCAPACITATED") then { [_unit, true] call unitSetReviveState; _unit call addPlayerHoldRevive; }; _damage = 0; }; _damage; }]; }; "Now wait just a minute, Dread, I don't have any friends to play Arma with and it's just me and my AI squad all the time" Well don't you worry because that is up next. This is a full-featured revive system after all. Here is a function that will order a supplied unit to revive a supplied body; the unit will run over to the dying unit, perform the same animations the player does, and the unit will rise again to continue being cannon fodder to Opfor (looking at you, player). The second code block is simply a wrapper for "addAction" syntax so I didn't need to mangle the first function due to weird arguments from addAction: Spoiler unitReviveBody = { params ["_unit", "_body"]; private ["_relativeDir", "_pos", "_leader"]; _unit groupChat format ["I am going to revive %1 %2, cover me!", [toUpper ((rank _body) select [0,1]), toLower ((rank _body) select [1, count (rank _body) - 1])] joinString "", (name _body splitString " ") # ((count (name _body splitString " ")) - 1)]; _relativeDir = _body getDir _unit; _pos = getPos _body; _unit doMove _pos; waitUntil {moveToCompleted _unit}; _unit setFormDir (_unit getDir _body); sleep 0.5; doStop _unit; //stop here prevents unit from making radio messages after doMove _body setVariable ["DE_REVIVING", true]; _unit playMove "AinvPknlMstpSnonWnonDnon_medic1"; _unit playMove "AinvPknlMstpSnonWnonDnon_medic2"; sleep 11.764; _unit playMoveNow "AinvPknlMstpSnonWnonDnon_medicEnd"; _body setVariable ["DE_REVIVING", nil]; [_body, false] call unitSetReviveState; _body setDamage 0; _leader = leader _unit; _unit setFormDir (_unit getDir _leader); _unit doFollow _leader; }; unitReviveBodyAction = { [_this # 0, _this # 3] call unitReviveBody; (_this # 0) removeAction (_this # 2); }; So there you have it, I have now given you all the pieces you need for a revive system. How you script them together is up to you...Alright, look, since I am a really nice guy, I'll also give you some sample stuff that will just work right out of the box. This bit of code will add revive icons to all dying units, and even switch it to a healing icon when someone is reviving: Spoiler addMissionEventHandler ["Draw3D", { allUnits apply { if (lifeState _x == "INCAPACITATED") then { _icon = "a3\ui_f\data\igui\cfg\holdactions\holdaction_revivemedic_ca.paa"; _color = [1,0,0,1]; if (!isNil {_x getVariable "DE_REVIVING"}) then { _icon = "a3\ui_f\data\igui\cfg\holdactions\holdaction_revive_ca.paa"; _color = [1,1,0,1]; }; drawIcon3D [_icon, _color, ASLToAGL (aimPos _x), 1, 1, 1, "", true, 0, "RobotoCondensed", "", true]; }; }; }]; Look I know I said: Quote if the code wasn't required, it's not here But I also said: Quote (with some tolerance to fun stuff that I actually did add anyway) And this is one of those things Next up, this piece of code will add a revive action for each nearby (50 meters by default) dying guy to all AI squadmates. If the unit goes further than 50 meters away, the action is also cleaned up (removed). This is a safeguard, because one of the biggest annoyances I've found so far with existing revive systems is that while it is possible to order AI squadmates to use actions (6 in the command menu), like reviving, it is actually kind of finnicky to get the option to show up, you basically have to order the unit to stand right next to/on top of whatever to use the action and by the time you get it working you might as well have just done it yourself. Bonus tip, there is a sleep 0.01 that is strictly there to allow the thread to suspend so any other scripts you have can run, because I'm a really nice guy, remember? So yeah here it is: Spoiler [] spawn { private ["_deadMan","_actionIndex"]; while {true} do { { if (lifeState _x == "INCAPACITATED") then { _deadMan = _x; (units player) apply { if !(isPlayer _x) then { if (isNil {_x getVariable (str _deadMan)}) then { if (_x distance _deadMan < 50) then { //systemChat format ["Creating revive for %1", name _deadMan]; _actionIndex = _x addAction [format ["Revive %1", name _deadMan], unitReviveBodyAction, _deadMan, 1000, false, true, "", "!(isPlayer _this)"]; _x setVariable [str _deadMan, _actionIndex]; }; } else { if (_x distance _deadMan > 50) then { //systemChat "removed"; _x removeAction (_x getVariable (str _deadMan)); _x setVariable [str _deadMan, nil]; }; }; }; }; }; sleep 0.01; } forEach allUnits; sleep 1; }; }; Normally I hate nesting so many scopes, but in this case I'll allow it because it's so dang useful Lastly, here's some example stuff (in order). How to "kill" a unit without actually killing a unit (shoutout to the discord #scripting for giving me a simple solution). Make AI go from unit to unit reviving everybody (from the video)(hardcoded...figure it out yourself). How to actually add the revive system to a unit (literally just call addRevive and pass in the unit). And the coup de grace, a loop that will automatically order the squadmate to revive the player should he "die"(also hardcoded) : Spoiler player addAction ["Kill player", { player setDamage 0.99; player setVelocityModelSpace [0,0,-10]; }, nil, 9, false, true]; player addAction ["Start revive loop", { { if (lifeState _x == "INCAPACITATED") then { [man_3, _x] call unitReviveBody; }; } forEach allUnits; }, nil, 10, false, true]; [player, man, man_1, man_2, man_3] apply { _x call addRevive}; [] spawn { while {true} do { if (lifeState player == "INCAPACITATED") then { //sleep 0.01; [man_3, player] call unitReviveBody; man_3 removeAction (man_3 getVariable (str player)); }; sleep 1; }; }; Well now, if you've made it this far then you must be really interested, here's the whole script (+ GitHub mirror) : Spoiler addPlayerClickRevive = { _this addAction [format ["Revive %1 %2", [toUpper ((rank _this) select [0,1]), toLower ((rank _this) select [1, count (rank _this) - 1])] joinString "", (name _this splitString " ") # ((count (name _this splitString " ")) - 1)], { params ["_target", "_caller", "_actionId", "_arguments"]; _caller playMove "AinvPknlMstpSnonWnonDnon_medic1"; _caller playMove "AinvPknlMstpSnonWnonDnon_medic2"; _caller playMove "AinvPknlMstpSnonWnonDnon_medicEnd"; sleep 10; [_target, false] call unitSetReviveState; _target setDamage 0; }, nil, 1000, false, true, "", "lifeState _target == 'INCAPACITATED' && _this == player"]; }; addPlayerHoldRevive = { [_this, format ["revive %1 %2", [toUpper ((rank _this) select [0,1]), toLower ((rank _this) select [1, count (rank _this) - 1])] joinString "", (name _this splitString " ") # ((count (name _this splitString " ")) - 1)], "a3\ui_f\data\igui\cfg\holdactions\holdaction_revivemedic_ca.paa", "a3\ui_f\data\igui\cfg\holdactions\holdaction_revive_ca.paa", "lifeState _target == 'INCAPACITATED' && _this == player && {_target != player && {_target distance _this < 3}}", "true", { (_this # 1) playMove "AinvPknlMstpSnonWnonDnon_medic1"; (_this # 1) playMove "AinvPknlMstpSnonWnonDnon_medic2"; (_this # 1) playMove "AinvPknlMstpSnonWnonDnon_medic3"; }, nil, { [_this # 0, false] call unitSetReviveState; (_this # 0) setDamage 0; (_this # 1) playMoveNow "AinvPknlMstpSnonWnonDnon_medicEnd"; }, { (_this # 1) playMoveNow "AinvPknlMstpSnonWnonDnon_medicEnd" }, nil, 14, 1000, true, true, true] call BIS_fnc_holdActionAdd; }; addRevive = { _this addEventHandler ["HandleDamage", { params ["_unit", "_selection", "_damage", "_source", "_projectile", "_hitIndex", "_instigator", "_hitPoint"]; if (_damage >= 0.99) then { if !(lifeState _unit == "INCAPACITATED") then { [_unit, true] call unitSetReviveState; _unit call addPlayerHoldRevive; _unit setVariable ["DE_TIME_KILLED", time]; }; _damage = 0; }; _damage; }]; }; unitSetReviveState = { params ["_unit", "_revive"]; if (_revive) then { _unit setUnconscious true; _unit setCaptive true; _unit allowDamage false; } else { _unit setUnconscious false; _unit setCaptive false; _unit allowDamage true; }; }; unitReviveBody = { params ["_unit", "_body"]; private ["_relativeDir", "_pos", "_leader"]; _unit groupChat format ["I am going to revive %1 %2, cover me!", [toUpper ((rank _body) select [0,1]), toLower ((rank _body) select [1, count (rank _body) - 1])] joinString "", (name _body splitString " ") # ((count (name _body splitString " ")) - 1)]; _relativeDir = _body getDir _unit; _pos = getPos _body; _unit doMove _pos; waitUntil {moveToCompleted _unit}; _unit setFormDir (_unit getDir _body); sleep 0.5; doStop _unit; //stop here prevents unit from making radio messages after doMove _body setVariable ["DE_REVIVING", true]; _unit playMove "AinvPknlMstpSnonWnonDnon_medic1"; _unit playMove "AinvPknlMstpSnonWnonDnon_medic2"; sleep 11.764; _unit playMoveNow "AinvPknlMstpSnonWnonDnon_medicEnd"; _body setVariable ["DE_REVIVING", nil]; [_body, false] call unitSetReviveState; _body setDamage 0; _leader = leader _unit; _unit setFormDir (_unit getDir _leader); _unit doFollow _leader; }; unitReviveBodyAction = { [_this # 0, _this # 3] call unitReviveBody; (_this # 0) removeAction (_this # 2); }; [] spawn { private ["_deadMan","_actionIndex"]; while {true} do { { if (lifeState _x == "INCAPACITATED") then { _deadMan = _x; (units player) apply { if !(isPlayer _x) then { if (isNil {_x getVariable (str _deadMan)}) then { if (_x distance _deadMan < 50) then { //systemChat format ["Creating revive for %1", name _deadMan]; _actionIndex = _x addAction [format ["Revive %1", name _deadMan], unitReviveBodyAction, _deadMan, 1000, false, true, "", "!(isPlayer _this)"]; _x setVariable [str _deadMan, _actionIndex]; }; } else { if (_x distance _deadMan > 50) then { //systemChat "removed"; _x removeAction (_x getVariable (str _deadMan)); _x setVariable [str _deadMan, nil]; }; }; }; }; }; sleep 0.01; } forEach allUnits; sleep 1; }; }; player addAction ["Start revive loop", { { if (lifeState _x == "INCAPACITATED") then { [man_3, _x] call unitReviveBody; }; } forEach allUnits; }, nil, 10, false, true]; player addAction ["Kill player", { player setDamage 0.99; player setVelocityModelSpace [0,0,-10]; }, nil, 9, false, true]; [] spawn { while {true} do { if (lifeState player == "INCAPACITATED") then { //sleep 0.01; [man_3, player] call unitReviveBody; man_3 removeAction (man_3 getVariable (str player)); }; sleep 1; }; }; addMissionEventHandler ["Draw3D", { allUnits apply { if (lifeState _x == "INCAPACITATED") then { _icon = "a3\ui_f\data\igui\cfg\holdactions\holdaction_revivemedic_ca.paa"; _color = [1,0,0,1]; if (!isNil {_x getVariable "DE_REVIVING"}) then { _icon = "a3\ui_f\data\igui\cfg\holdactions\holdaction_revive_ca.paa"; _color = [1,1,0,1]; }; drawIcon3D [_icon, _color, ASLToAGL (aimPos _x), 1, 1, 1, "", true, 0, "RobotoCondensed", "", true]; }; }; }]; [player, man, man_1, man_2, man_3] apply { _x call addRevive}; //man call addPlayerClickRevive; [man, man_1, man_2] apply { _x setDamage 0.99; _x setVelocityModelSpace [0,0,-7] }; Enjoy! I truly can't wait to see what you make with this, please send me a link or pm Edited May 2, 2022 by dreadedentity adding bold message at the beginning notifying readers to find updated code on github 5 5 Share this post Link to post Share on other sites
dreadpirate 173 Posted April 18, 2022 So, I made a little skirmish with a squad of DE_Revive enabled friendlies against respawning waves of enemy AI. Generally, things worked well. I could revive AI and I could order AI to revive each other. A few little things though: I accidentally ordered an AI to revive himself, which of course did nothing. But, when he did eventually get revived, he played the revive animation The "Revive x" addAction wasn't getting removed properly sometimes, cluttering up the AI order menu with revive actions for units that were already revived When I was downed in heavy contact, the AI was too focused on fighting to send someone to revive me. Even after they dealt with the threat, I was lying there and nobody came. Eventually I had to respawn Share this post Link to post Share on other sites
dreadedentity 278 Posted April 18, 2022 10 hours ago, dreadpirate said: Generally, things worked well. I could revive AI and I could order AI to revive each other. A few little things though Hey Dread! Thanks for taking the time to check this out and letting me know where it's falling short. #1 & #2 definitely sound like oversight on my part so I will have to do another pass on the logic. For #2 I think I even know where the starting point is, I bet it has to do with the player reviving units and their actions not being cleaned up, which is probably also how #1 happened now that I think about it. For #3, I simply wanted to give the pieces for a revive system and let people take it further into how they see fit. The auto-reviving stuff that I put in was just meant as an example, proof that it all works, and only even tested with 1 squadmate. I wanted to leave it up to users of the system to decide how the squad should decide to revive other units in their mission. Maybe the auto-revive only works with units from the same squad, and leader must manually order reviving for anyone else. Should a squadmate immediately try to revive in the heat of battle or wait until a certain combatBehavior like "SAFE", maybe you'd use a combination of that and combatMode. Maybe reviving the player is a priority revive, but random squaddies can wait until after the battle. Does reviving cost a FAK, or require a medkit? Can only combat medics revive or everyone? This system does not even eventually kill you, as I deemed that not required, in other words you would lay there forever if your auto-revive method didn't retry, I figured that anyone with even a semi-decent level of scripting could easily build that into this, I mean, I already have a while loop that ticks roughly once a second I feel like there are way too many options for me to try to build a generic auto-revive that you just run with parameters, not to mention someone might come along one day with a cool idea that I never thought of. So I just built a little proof of concept and leave it up to the mission maker to decide how it should work By the way, to all future readers, any edits that I make to this can be found on the Github page, as I likely will not bother to update the original post beyond adding a bolded message at the top 1 Share this post Link to post Share on other sites
_foley 192 Posted April 18, 2022 Nicely written, simple and easy to modify. So in terms of use cases, if I understand correctly this is meant to be a singleplayer version of vanilla revive, right? Share this post Link to post Share on other sites
dreadedentity 278 Posted April 18, 2022 5 hours ago, _foley said: this is meant to be a singleplayer version of vanilla revive, right? Not necessarily; While I built and tested this in singleplayer, I would want this to work in an MP environment, although I'll be the first to admit that I've never really practiced MP-safe scripting. That said, this will likely not work in MP at the moment, without some edits to the way certain commands are called. But that's not to say there are not advantages to this over the vanilla system because there are a few: Apparently vanilla system only works for players? This gives you the ability to have NPC units revivable and gives them the ability to perform the reviving on others or the player Of course, this all works in singleplayer already, without needing any changes, although there is at least 1 bug in there somewhere to fix. I think the vanilla system only works in MP This is a completely open system, all code is available to see and edit, perform enhancements and add new features, or just give my code a harsh critique Infinite flexibility as a consequence of the above item; while the vanilla system gives you some options right out of the box and this does not, you have infinite freedom in how the system will operate. If you can dream it, and code it, then you can do it 1 Share this post Link to post Share on other sites
_foley 192 Posted April 18, 2022 Good points! Regarding making the script multiplayer-ready, here's a few pointers that will give you a good idea of what needs changing: 1. setCaptive takes a local argument, meaning that it needs to be executed on casualty's machine whereas now it's executed on healer's machine. 2. addAction has local effects, meaning that it needs to be executed on every machine where this action should show up. Currently action in HandleDamage EH is added only on casualty's machine. Luckily, remoteExec allows you to run any command / function and specify which machine(s) should it run on. 1 Share this post Link to post Share on other sites
dreadedentity 278 Posted April 18, 2022 Dropping in to say that instead of working, (wfh, am I right?), I've just pushed a fix to the github repo for #1 & #2 from dreadpirate's post, which were really the same problem. It's quick n dirty, but it works Also added something I realized was missing: Self-revive + SAMPLE addAction showing how to use the new code block Going to start working on making this MP compatible pretty soon, unless I think up any more features this urgently(in my opinion) needs Keep those bug reports coming, if there are any more Share this post Link to post Share on other sites
fn_Quiksilver 1636 Posted April 19, 2022 welcome back 🙂 only suggestion i have is to look at the handleDamage code and see if it is having the effect you want (at the moment, damage to his hands is treated with same severity as damage to head) 1 Share this post Link to post Share on other sites
dreadedentity 278 Posted May 2, 2022 On 4/19/2022 at 9:55 AM, fn_Quiksilver said: at the moment, damage to his hands is treated with same severity as damage to head Thanks Quiksilver, I think something like this should be decided by the creator, not by the systems they are using in their mission. Currently, this will always put the unit into a "downed" state once damage reaches the threshold where the unit would normally die. I think it would be pretty simple to make changes to cause damage to certain body parts to do different things, but for the purposes of demonstration I wanted to make sure all units that have "revivability" are handled Share this post Link to post Share on other sites
dreadedentity 278 Posted May 2, 2022 I believe I have made all the changes necessary for this to be working in multiplayer. You will always be able to "get latest" on Github. Here are the changes I've made: Made use of the Functions Library to hold the system's code Replaced most, if not all command calls with remoteExec'd version while keeping in mind locality during runtime Split up the Event Script init.sqf into initPlayerLocal.sqf and initServer.sqf (not strictly required, it just helped me debug) Tested in all game environments (single-player, local multiplayer, dedicated server multiplayer) I want this to work in all environments (single-player, locally hosted multiplayer, and dedicated server multiplayer) so please let me know if there is any part of the system that is not working. Remember your locality, because it gets exponentially more difficult when you're in a multiplayer environment. Keep in mind that I am only going to fix stuff related to this revive system (I am not going to debug your mission). I cannot be solely responsible to fix problems in user-edits for the same reason; of course you can still ask me, but I'm not going to give it the same level of effort as an issue in the base code I set the player revive action to 2 seconds during testing and I forgot to change it back, so in fn_addPlayerHoldRevive.sqf make sure you set the 2 back to it's default of 14, or whatever number you'd like to use. I will probably fix this in subsequent commits I will update the original post with a clear message at the beginning that the latest version can be found on Github Enjoy! 3 Share this post Link to post Share on other sites