TargetSpy
Table of Contents
- 1. About
- 2. License
- 3. Options
- 4. Source
- 4.1. Finders
- 4.2. Filters
- 4.3. Getters
- 4.4. Views
- 4.5. Core
- 4.5.1. Event handler
- 4.5.2. Turn on/off
- 4.5.3. Information piece
- 4.5.4. Class list
- 4.5.5. Existing Cvar
- 4.5.6. Utilities
- 4.5.7. Target screen position interpolator
- 4.5.8. Class utilities
- 4.5.9. Follower classes
- 4.5.10. Projector
- 4.5.11. View utilities
- 4.5.12. Menu classes
- 4.5.13. Configuration
- 4.5.14. Extension replacements
- 5. Project setup
- 6. Tests
1. About
TargetSpy shows enemy health, tag, and other things.
TargetSpy is a part of DoomToolbox.
Features:
- extensible modular structure;
- target finders: autoaim, shootable, non-shootable, no blockmap;
- target filters: blocklist, monsters, light level, health, hidden, enemies, dormant, active;
- target information: health, max health, armor, class, tag, flags;
- target frame;
- text crosshair with options and PreciseCrosshair support.
1.1. Extending TargetSpy
1.1.1. Finders, filters, getters, and views
TargetSpy can be extended by declaring special classes. To be detected by TargetSpy, a class must inherit Actor and meet specific requirements, listed below.
There are four types of TargetSpy extension classes:
- Finders find targets. They must have a name that starts with
tsFinder_and havestatic Actor find()function. - Filters remove unwanted targets. They must have a name that starts with
tsFilter_and havestatic bool isAllowed(Actor)function. Getters collect information about a target. They must have a name that starts with
tsGetter_and havestatic int, string, int, int makeInformation(Actor)function. This function returns:- int: value of
ts_Kindenum, which tells what kind of information is provided; - string: string value, used only for
ts_Kind.Text; - int: current value, used for
ts_Kind.Value,ts_Kind.ValueAndMax, andts_Kind.Bar; - int: max value, used for
ts_Kind.ValueAndMaxandts_Kind.Bar.
Additionally, a getter can have
static Font getFont(Actor)function to select a font for an actor.- int: value of
- Views show information on screen. They must have a name that starts with
tsView_and havestatic ui void draw(Array<ts_InformationPiece>informationPieces, double alpha, RenderEvent event, vector3 targetPosition,vector2 targetSize)function. See here for reference whatts_InformationPiececontains.
Note that Finders, Filters, and Getters classes don't depend on TargetSpy types in any way, and therefore can be safely included in other mods to provide TargetSpy integration when loaded together.
Extension classes are not meant to be instantiated, only their static functions are
used. Extension classes can have a translatable user string defined in LANGUAGE
lump. The string key is simply a class name. Extension classes are automatically
added to TargetSpy option menus, and their toggle state is saved in game
configuration.
All extensions are automatically added to the corresponding toggle menus, from where they can be turned on and off.
In fact, all out-of-the-box TargetSpy finders, filters, getters, and views are implemented as extensions, and can be used as examples for implementing custom extensions.
1.1.2. Blocklist
To make TargetSpy ignore classes, put them into ts_blocklist lump. Any number of
ts_blocklist lumps can be loaded. Blocklist filter must be enabled.
1.1.3. Follower classes
To make TargetSpy to recognize actors that represent parts of another actors, like
headshots and enemy parts in some mods, put follower classes in ts_followers lump.
Follower classes must have the real enemy actor set in their master field.
2. License
SPDX-FileCopyrightText: © 2026 Alexander Kromm <mmaulwurff@gmail.com> SPDX-License-Identifier: GPL-3.0-only
3. Options
ts_OptionsTitle = "TargetSpy \cd⌖";
AddOptionMenu OptionsMenu { Submenu "$ts_OptionsTitle", ts_Options } AddOptionMenu OptionsMenuSimple { Submenu "$ts_OptionsTitle", ts_Options } OptionMenu ts_Options { Title "$ts_OptionsTitle" ts_PlainTranslator Option "TargetSpy on", ts_on, OnOff StaticText "" StaticText "Toggles" Submenu "Finders", ts_FinderToggles Submenu "Filters", ts_FilterToggles Submenu "Getters", ts_GetterToggles Submenu "Views", ts_ViewToggles StaticText "" Submenu "Filter options", ts_FilterOptions Submenu "Getters order", ts_GettersOrder Submenu "Text view options", ts_TextViewOptions Submenu "Frame options", ts_FrameOptions Submenu "Crosshair options", ts_CrosshairOptions }
4. Source
4.1. Finders
Range for finding targets:
4000.0
4.1.1. Aim targets
tsFinder_Aim = "Aim target";
class tsFinder_Aim : Actor { static Actor find() { return players[consolePlayer].mo.aimTarget(); } }
4.1.2. Shootable
tsFinder_Shootable = "Shootable only";
class tsFinder_Shootable : Actor { static Actor find() { PlayerInfo player = players[consolePlayer]; PlayerPawn pawn = player.mo; FLineTraceData lineTraceData; double angle = pawn.angle; double pitch = pawn.pitch; double viewHeight = player.viewHeight; pawn.lineTrace(angle, 4000.0, pitch, 0, viewHeight, 0.0, 0.0, lineTraceData); return lineTraceData.hitActor; } }
4.1.3. Non-shootable
tsFinder_NonShootable = "Non-shootable too";
class tsFinder_NonShootable : Actor { static Actor find() { PlayerPawn pawn = players[consolePlayer].mo; FTranslatedLineTarget lineTarget; pawn.aimLineAttack(pawn.angle, 4000.0, lineTarget, 0, AIM_FLAGS); return lineTarget.lineTarget; } const AIM_FLAGS = ALF_CheckNonShootable | ALF_ForceNoSmart; }
4.1.4. Noblockmap actor finder
tsFinder_Noblockmap = "Everything";
// SPDX-FileCopyrightText: © 2020 proydoha class tsFinder_Noblockmap : Actor { static Actor find() { PlayerInfo player = players[consolePlayer]; return lineAttackNoBlockmap(player.mo, player.viewheight); } static Actor lineAttackNoBlockmap(Actor a, double offsetz) { FLineTraceData lineTraceData; a.LineTrace(a.angle, 4000.0, a.pitch, 0, offsetz, 0.0, 0.0, lineTraceData); if (lineTraceData.HitType == TRACE_HitActor) { return NULL; } ThinkerIterator noBlockmapActors = ThinkerIterator.Create(); Actor nbmActor; Actor closestNbmActor; while (nbmActor = Actor(noBlockmapActors.Next())) { if (!nbmActor.bNoBlockmap) { continue; } // Do not target inventory items that belong to somebody. let inv = Inventory(nbmActor); if (inv && inv.Owner) { continue; } // Detect NoBlockmap actors by checking if line from LineTrace // intersects sphere they are in. // // Line equation is: // P = LineStart + Direction * t // // Sphere equation is: // (P - SphereCenter) dot (P - SphereCenter) = SphereRadius * SphereRadius // // Line and Sphere share points (P) if they intersect: // // Combined equation: // (LineStart + Direction * t - SphereCenter) // dot (LineStart + Direction * t - SphereCenter) // // Same equation rearranged: // t * t * (Direction dot Direction) + // + 2 * t * (Direction dot (LineStart - SphereCenter)) + // + ((LineStart - SphereCenter) dot (LineStart - SphereCenter)) - // - SphereRadius * SphereRadius = 0 // // This is quadratic equation: // t * t * a + t * b + c = 0 vector3 sphereCenter = (nbmActor.pos.x, nbmActor.pos.y, nbmActor.pos.z + nbmActor.height/2); double sphereRadius = max(nbmActor.height, nbmActor.radius * 2) / 2; vector3 lineStart = (a.pos.x, a.pos.y, a.pos.z + offsetz); vector3 lineEnd = lineTraceData.HitLocation; vector3 direction = (lineEnd - lineStart).Unit(); // a, b, c of the quadratic equation: double a = direction dot direction; double b = 2 * (direction dot (lineStart - sphereCenter)); double c = (lineStart - sphereCenter) dot (lineStart - sphereCenter) - sphereRadius * sphereRadius; // Line intersects or touches Sphere if t has solutions // t has solution(s) if discriminant >= 0 // discriminant = b * b - 4 * a * c // t = ( -b ± sqrt(discriminant) ) / 2 * a double discriminant = b * b - 4 * a * c; bool isDiscriminantNonNegative = (discriminant >= 0); if (!isDiscriminantNonNegative) { continue; } double t1 = (-b + sqrt(discriminant)) / (2 * a); double t2 = (-b - sqrt(discriminant)) / (2 * a); // if both of those solutions are positive target is in front of the player bool areSolutionsPositive = (t1 > 0 && t2 > 0); if (!areSolutionsPositive) { continue; } // Discard actors that are further than lineEnd (most likely behind the wall) bool isFurther = ((lineStart - lineEnd).Length() < (lineStart - nbmActor.pos).Length()); if (isFurther) { continue; } if (closestNbmActor == NULL) { closestNbmActor = nbmActor; continue; } // Pick an actor closest to the player if ((LineStart - nbmActor.pos).Length() < (LineStart - closestNbmActor.pos).Length()) { closestNbmActor = nbmActor; } } return closestNbmActor; } }
4.2. Filters
4.2.1. Blocklist
tsFilter_Blocklist = "Blocklist";
class tsFilter_Blocklist : Actor { static bool isAllowed(Actor anActor) { string className = anActor.getClassName(); return !ts_EventHandler.getInstance().getBlocklist().contains(className); } }
Autoautosave
AutoautosaveAlertToken AutoautosaveAlerter AutoautosaveBossAlerter AutoautosaveToken m8f_aas_token
The Adventures of Square
Cow CowboyCow MummyCow WinterCow SpaceCow SpaceCowFloating
???
ShieldDefense ShieldDefense2 VoidField
Universal Gibs
UGGib_Corpse_Shootable
4.2.2. Monsters
tsFilter_Monsters = "Monsters only";
class tsFilter_Monsters : Actor { static bool isAllowed(Actor anActor) { return anActor.bIsMonster; } }
4.2.3. Light level
tsFilter_LightLevel = "Lit only";
class tsFilter_LightLevel : Actor { static bool isAllowed(Actor anActor) { PlayerPawn pawn = players[consolePlayer].mo; if (pawn.findInventory("PowerLightAmp") || pawn.findInventory("PowerInvulnerable")) return true; Sector aSector = anActor.curSector; int lightLevel = aSector.lightLevel; ts_Options options = ts_EventHandler.getInstance().getOptions(); return lightLevel >= options.getMinLightLevel(); } }
int getMinLightLevel() const { return mMinLightLevelCvar.getInt(); }
private Cvar mMinLightLevelCvar;
mMinLightLevelCvar = ts_ExistingCvar.find("ts_min_light_level");
user int ts_min_light_level = 120;
Slider "Minimal light level", ts_min_light_level, 0, 250, 10, 0
4.2.4. Health
tsFilter_Health = "Positive health only";
class tsFilter_Health : Actor { static bool isAllowed(Actor anActor) { return anActor.health > 0; } }
4.2.5. Hidden
tsFilter_Hidden = "Not hidden only";
class tsFilter_Hidden : Actor { static bool isAllowed(Actor anActor) { return !anActor.bShadow && !anActor.bStealth; } }
4.2.6. Enemies
tsFilter_Enemies = "Enemies only";
class tsFilter_Enemies : Actor { static bool isAllowed(Actor anActor) { return !anActor.bFriendly; } }
4.2.7. Dormant
tsFilter_Dormant = "Not dormant only";
class tsFilter_Dormant : Actor { static bool isAllowed(Actor anActor) { return !anActor.bDormant; } }
4.2.8. Active
tsFilter_Active = "Active only";
class tsFilter_Active : Actor { static bool isAllowed(Actor anActor) { return anActor.target != NULL; } }
4.3. Getters
4.3.1. Health
tsGetter_Health = "Health";
tsGetter_Health = "Здоровье";
class tsGetter_Health : Actor { static int, string, int, int makeInformation(Actor anActor) { return ts_Kind.Value, "", anActor.health, 0; } }
4.3.2. Health and max health
tsGetter_HealthAndMax = "Health and max health";
class tsGetter_HealthAndMax : Actor { static int, string, int, int makeInformation(Actor anActor) { int maxHealth = ts_Utils.getActorMaxHealth(anActor); return ts_Kind.ValueAndMax, "", anActor.health, maxHealth; } }
4.3.3. Silent health and max health
This getter is somewhat special. It is intended to:
- carry health and max health even when other health getters are disabled.
- be invisible.
- be always on.
class tsGetter_SilentHealth : Actor { static int, string, int, int makeInformation(Actor anActor) { int maxHealth = ts_Utils.getActorMaxHealth(anActor); return ts_Kind.Empty, "", anActor.health, maxHealth; } }
{
"tsGetter_SilentHealth": "on"
}
4.3.4. Health bar
tsGetter_HealthBar = "Health bar";
class tsGetter_HealthBar : Actor { static int, string, int, int makeInformation(Actor anActor) { int maxHealth = ts_Utils.getActorMaxHealth(anActor); return ts_Kind.Bar, "", anActor.health, maxHealth; } }
4.3.5. Armor bar
tsGetter_ArmorBar = "Armor bar";
class tsGetter_ArmorBar : Actor { static int, string, int, int makeInformation(Actor anActor) { let armor = BasicArmor(anActor.findInventory("BasicArmor")); if (armor == NULL || armor.amount == 0) return ts_Kind.Empty, "", 0, 0; return ts_Kind.Bar, "", armor.amount, armor.actualSaveAmount; } }
4.3.6. Class
tsGetter_Class = "Class name";
class tsGetter_Class : Actor { static int, string, int, int makeInformation(Actor anActor) { return ts_Kind.PlainText, anActor.getClassName(), 0, 0; } }
4.3.7. Tag
tsGetter_Tag = "Tag";
class tsGetter_Tag : Actor { static int, string, int, int makeInformation(Actor anActor) { string tag = anActor.player ? anActor.player.getUserName() : anActor.getTag(); return ts_Kind.PlainText, tag, 0, 0; } // Example: //static Font getFont(Actor anActor) //{ // return Font.findFont(anActor is "Zombieman" ? "BigFont" : "NewSmallFont"); //} }
4.3.8. Tag and class
tsGetter_TagAndClass = "Tag (class)";
class tsGetter_TagAndClass : Actor { static int, string, int, int makeInformation(Actor anActor) { string tag = anActor.player ? anActor.player.getUserName() : anActor.getTag(); string aClass = anActor.getClassName(); string result = (aClass != tag) ? string.format("%s (%s)", tag, aClass) : string.format("%s", aClass); return ts_Kind.PlainText, result, 0, 0; } }
4.3.9. Flags
tsGetter_Flags = "Flags"; ts_Friendly = "Friendly"; ts_Invulnerable = "Invulnerable"; ts_Boss = "Boss"; ts_Dormant = "Dormant"; ts_Buddha = "Buddha"; ts_Undamageable = "Undamageable"; ts_NoBlockmap = "No blockmap";
ts_Boss = "Босс";
class tsGetter_Flags : Actor { static int, string, int, int makeInformation(Actor anActor) { string result; if (anActor.bFriendly && !anActor.player) append(result, translate("ts_Friendly")); if (anActor.bInvulnerable) append(result, translate("ts_Invulnerable")); if (anActor.bBoss ) append(result, translate("ts_Boss")); if (anActor.bDormant ) append(result, translate("ts_Dormant")); if (anActor.bBuddha ) append(result, translate("ts_Buddha")); if (anActor.bNoDamage ) append(result, translate("ts_Undamageable")); if (anActor.bNoBlockmap ) append(result, translate("ts_NoBlockmap")); return ts_Kind.PlainText, result, 0, 0; } private static string translate(string tag) { return StringTable.localize(tag, false); } private static void append(out string result, string appendix) { if (result.length() == 0) result = appendix; else result.appendFormat(", %s", appendix); } }
4.4. Views
4.4.1. Fader
class ts_Fader { void initialize() { mElapsed = FADE_TIME_TICS; } double getAlpha(double fracTic) const { return min(1, (FADE_TIME_TICS - (mElapsed + fracTic)) / FADE_TIME_TICS); } void reset() { mElapsed = -2; } // -2 for one-tick buffer to prevent flickering. void tick() { ++mElapsed; } const FADE_TIME_TICS = TICRATE / 3; private double mElapsed; }
4.4.2. Text view
tsView_Text = "Plain text";
class tsView_Text : Actor { static ui void draw(Array<ts_InformationPiece> informationPieces, double alpha, RenderEvent event, vector3 targetPosition, vector2 targetSize) { if (alpha <= 0.0) return; if (informationPieces.size() == 0) return; ts_Options options = ts_EventHandler.getInstance().getOptions(); double scale = options.getScale(); Array<string> lines; Array<double> lineWidths; Array<double> lineHeights; Array<Font> fonts; double totalWidth = 0; double totalHeight = 0; foreach (piece : informationPieces) { string line = ts_ViewUtils.toText(piece, options); if (line.length() == 0) continue; lines.push(line); double lineWidth = round(scale * piece.font.stringWidth(line)); double lineHeight = round(scale * piece.font.getHeight()); totalWidth = max(totalWidth, piece.font.stringWidth(line)); totalHeight += lineHeight; lineWidths.push(lineWidth); lineHeights.push(lineHeight); fonts.push(piece.font); } int linesCount = lines.size(); if (linesCount == 0) return; // Note: stringWidth underestimates. totalWidth = totalWidth * 1.1 * scale; int screenWidth = Screen.getWidth(); int screenHeight = Screen.getHeight(); vector2 position = getPosition(options, event, targetPosition, targetSize); if (options.getPlace() == ts_Options.PlaceAboveTarget) position.y -= totalHeight; position.x = clamp(position.x, totalWidth / 2, screenWidth - totalWidth / 2); int color = options.isUsingHealthBarColors() ? ts_Utils.getHealthColor(options, informationPieces) : Font.CR_Gray; bool isBackgroundEnabled = options.isBackgroundEnabled(); for (int i = 0; i < linesCount; ++i) { double lineHeight = lineHeights[i]; position.y = clamp(position.y, 0, screenHeight - totalHeight + lineHeight); if (isBackgroundEnabled) { double additionalWidth = fonts[i].stringWidth(" ") * scale; ts_ViewUtils.drawBackground(lineWidths[i] + additionalWidth, lineHeight, position, alpha); } ts_ViewUtils.drawText(fonts[i], lines[i], lineWidths[i], position, alpha, scale, color); position.y += lineHeight; } } static ui vector2 getPosition(ts_Options options, RenderEvent event, vector3 targetPosition, vector2 targetSize) { int place = options.getPlace(); if (place == ts_Options.PlaceFixed) return (options.getXPosition() * Screen.getWidth(), options.getYPosition() * Screen.getHeight()); ts_EventHandler eventHandler = ts_EventHandler.getInstance(); ts_Projector projector = eventHandler.getProjector(); if (place == ts_Options.PlaceAboveTarget) targetPosition.z += targetSize.y * 1.1; // Room for sprites above height. return eventHandler .interpolateTextScreenPosition(projector.makePosition(event, targetPosition)); } }
static ui void draw(Array<ts_InformationPiece> informationPieces, double alpha, RenderEvent event, vector3 targetPosition, vector2 targetSize) { if (alpha <= 0.0) return; if (informationPieces.size() == 0) return; ts_Options options = ts_EventHandler.getInstance().getOptions(); double scale = options.getScale(); Array<string> lines; Array<double> lineWidths; Array<double> lineHeights; Array<Font> fonts; double totalWidth = 0; double totalHeight = 0; foreach (piece : informationPieces) { string line = ts_ViewUtils.toText(piece, options); if (line.length() == 0) continue; lines.push(line); double lineWidth = round(scale * piece.font.stringWidth(line)); double lineHeight = round(scale * piece.font.getHeight()); totalWidth = max(totalWidth, piece.font.stringWidth(line)); totalHeight += lineHeight; lineWidths.push(lineWidth); lineHeights.push(lineHeight); fonts.push(piece.font); } int linesCount = lines.size(); if (linesCount == 0) return; // Note: stringWidth underestimates. totalWidth = totalWidth * 1.1 * scale; int screenWidth = Screen.getWidth(); int screenHeight = Screen.getHeight(); vector2 position = getPosition(options, event, targetPosition, targetSize); if (options.getPlace() == ts_Options.PlaceAboveTarget) position.y -= totalHeight; position.x = clamp(position.x, totalWidth / 2, screenWidth - totalWidth / 2); int color = options.isUsingHealthBarColors() ? ts_Utils.getHealthColor(options, informationPieces) : Font.CR_Gray; bool isBackgroundEnabled = options.isBackgroundEnabled(); for (int i = 0; i < linesCount; ++i) { double lineHeight = lineHeights[i]; position.y = clamp(position.y, 0, screenHeight - totalHeight + lineHeight); if (isBackgroundEnabled) { double additionalWidth = fonts[i].stringWidth(" ") * scale; ts_ViewUtils.drawBackground(lineWidths[i] + additionalWidth, lineHeight, position, alpha); } ts_ViewUtils.drawText(fonts[i], lines[i], lineWidths[i], position, alpha, scale, color); position.y += lineHeight; } } static ui vector2 getPosition(ts_Options options, RenderEvent event, vector3 targetPosition, vector2 targetSize) { int place = options.getPlace(); if (place == ts_Options.PlaceFixed) return (options.getXPosition() * Screen.getWidth(), options.getYPosition() * Screen.getHeight()); ts_EventHandler eventHandler = ts_EventHandler.getInstance(); ts_Projector projector = eventHandler.getProjector(); if (place == ts_Options.PlaceAboveTarget) targetPosition.z += targetSize.y * 1.1; // Room for sprites above height. return eventHandler .interpolateTextScreenPosition(projector.makePosition(event, targetPosition)); }
double getScale() const { return mScaleCvar.getFloat(); } double getXPosition() const { return mXPositionCvar.getFloat(); } double getYPosition() const { return mYPositionCvar.getFloat(); } enum Places { PlaceFixed, PlaceAboveTarget, PlaceBelowTarget } int getPlace() const { return mPlaceCvar.getInt(); } bool isBackgroundEnabled() const { return mBackgroundCvar.getInt(); } bool isUsingHealthBarColors() const { return mHealthColors.getInt(); }
private Cvar mScaleCvar; private Cvar mXPositionCvar; private Cvar mYPositionCvar; private Cvar mPlaceCvar; private Cvar mBackgroundCvar; private Cvar mHealthColors;
mScaleCvar = ts_ExistingCvar.find("ts_scale"); mXPositionCvar = ts_ExistingCvar.find("ts_x_position"); mYPositionCvar = ts_ExistingCvar.find("ts_y_position"); mPlaceCvar = ts_ExistingCvar.find("ts_place"); mBackgroundCvar = ts_ExistingCvar.find("ts_background"); mHealthColors = ts_ExistingCvar.find("ts_using_health_bar_colors");
user float ts_scale = 1.0; user float ts_x_position = 0.5; user float ts_y_position = 0.2; user int ts_place = 0; user bool ts_background = false; user bool ts_using_health_bar_colors = false;
OptionMenu ts_TextViewOptions { Title "Text view options" Slider "Scale", ts_scale, 0.1, 4.0, 0.1, 1 Slider "X position", ts_x_position, 0, 1.0, 0.01, 2 Slider "Y position", ts_y_position, 0, 1.0, 0.01, 2 Option "Place", ts_place, ts_Places Option "Background", ts_background, OnOff Option "Use health bar colors", ts_using_health_bar_colors, OnOff StaticText "" Submenu "Default bar options", ts_DefaultBarOptions Submenu "Health bar options", ts_HealthBarOptions Submenu "Armor bar options", ts_ArmorBarOptions } OptionValue ts_Places { 0, "Fixed" 1, "Above target" 2, "Below target" }
4.4.3. Frame
tsView_Frame = "Frame";
class tsView_Frame : Actor { static ui void draw(Array<ts_InformationPiece> informationPieces, double alpha, RenderEvent event, vector3 targetPosition, vector2 targetSize) { if (alpha <= 0.0) return; ts_EventHandler eventHandler = ts_EventHandler.getInstance(); ts_FrameMemory memory = eventHandler.getFrameMemory(); ts_Projector projector = eventHandler.getProjector(); ts_FrameOptions options = memory.getOptions(); ts_Options mainOptions = eventHandler.getOptions(); Font aFont = memory.getFont(); double scale = options.getScale(); int color = options.isUsingHealthBarColors() ? ts_Utils.getHealthColor(mainOptions, informationPieces) : Font.CR_Gray; double halfLineHeight = aFont.getHeight() * scale / 2; vector3 worldPositions[8] = { targetPosition + ( targetSize.x, targetSize.x, 0), targetPosition + ( targetSize.x, -targetSize.x, 0), targetPosition + (-targetSize.x, targetSize.x, 0), targetPosition + (-targetSize.x, -targetSize.x, 0), targetPosition + ( targetSize.x, targetSize.x, targetSize.y), targetPosition + ( targetSize.x, -targetSize.x, targetSize.y), targetPosition + (-targetSize.x, targetSize.x, targetSize.y), targetPosition + (-targetSize.x, -targetSize.x, targetSize.y) }; vector2 allScreenPositions[8]; for (int i = 0; i < 8; ++i) allScreenPositions[i] = projector.makePosition(event, worldPositions[i]); double left = double.max; double right = -1; double top = double.max; double bottom = -1; for (int i = 0; i < 8; ++i) { vector2 screenPosition = allScreenPositions[i]; left = min(left, screenPosition.x); right = max(right, screenPosition.x); top = min(top, screenPosition.y); bottom = max(bottom, screenPosition.y); } vector2 positions[4] = { (left, top - halfLineHeight), (right, top - halfLineHeight), (left, bottom - halfLineHeight), (right, bottom - halfLineHeight) }; for (int i = 0; i < 4; ++i) positions[i] = memory.interpolateScreenPosition(positions[i], i); for (int i = 0; i < 4; ++i) { string part = options.getFrame(i); double width = aFont.stringWidth(part) * scale; ts_ViewUtils.drawText(aFont, part, width, positions[i], alpha, scale, color); } } }
class ts_FrameMemory { void initialize(ts_TargetSource targetSource) { mFont = Font.findFont("NewSmallFont"); (mOptions = new("ts_FrameOptions")).initialize(); for (int i = 0; i < 4; ++i) { (mInterpolators[i] = new("ts_TargetScreenPositionInterpolator")) .initialize(targetSource); } } ui vector2 interpolateScreenPosition(vector2 position, int index) { return mInterpolators[index].interpolateScreenPosition(position); } ui Font getFont() const { return mFont; } ui ts_FrameOptions getOptions() const { return mOptions; } private Font mFont; private ts_TargetScreenPositionInterpolator[4] mInterpolators; private ts_FrameOptions mOptions; }
(mFrameMemory = new("ts_FrameMemory")).initialize(mCurrentTarget);
private ts_FrameMemory mFrameMemory; clearscope ts_FrameMemory getFrameMemory() { return mFrameMemory; }
class ts_FrameOptions { void initialize() { mCustomEnabled = ts_ExistingCvar.find("ts_frame_custom_enabled"); mSelected = ts_ExistingCvar.find("ts_frame_selected"); mCustomTopLeft = ts_ExistingCvar.find("ts_frame_custom_top_left"); mCustomTopRight = ts_ExistingCvar.find("ts_frame_custom_top_right"); mCustomBottomLeft = ts_ExistingCvar.find("ts_frame_custom_bottom_left"); mCustomBottomRight = ts_ExistingCvar.find("ts_frame_custom_bottom_right"); mScale = ts_ExistingCvar.find("ts_frame_scale"); mWithHealthColors = ts_ExistingCvar.find("ts_frame_using_health_bar_colors"); } string getFrame(int i) const { if (mCustomEnabled.getInt()) { switch (i) { case 0: return mCustomTopLeft.getString(); case 1: return mCustomTopRight.getString(); case 2: return mCustomBottomLeft.getString(); case 3: return mCustomBottomRight.getString(); default: return "."; } } int selectedFrame = clamp(mSelected.getInt(), 0, 7); switch (selectedFrame) { case 0: return mFrames0[i]; case 1: return mFrames1[i]; case 2: return mFrames2[i]; case 3: return mFrames3[i]; case 4: return mFrames4[i]; case 5: return mFrames5[i]; case 6: return mFrames6[i]; case 7: return mFrames7[i]; } return ""; } double getScale() const { return mScale.getFloat(); } bool isUsingHealthBarColors() const { return mWithHealthColors.getInt(); } private Cvar mCustomEnabled; private Cvar mSelected; private Cvar mCustomTopLeft; private Cvar mCustomTopRight; private Cvar mCustomBottomLeft; private Cvar mCustomBottomRight; private Cvar mScale; private Cvar mWithHealthColors; static const string mFrames0[] = {"▛", "▜", "▙", "▟"}; static const string mFrames1[] = {"┌", "┐", "└", "┘"}; static const string mFrames2[] = {"┏", "┓", "┗", "┛"}; static const string mFrames3[] = {"▞", "▚", "▚", "▞"}; static const string mFrames4[] = {"╔", "╗", "╚", "╝"}; static const string mFrames5[] = {"╭", "╮", "╰", "╯"}; static const string mFrames6[] = {"/", '\', '\', "/"}; static const string mFrames7[] = {"↘", "↙", "↗", "↖"}; }
user bool ts_frame_custom_enabled = false; user int ts_frame_selected = 0; user string ts_frame_custom_top_left = "."; user string ts_frame_custom_top_right = "."; user string ts_frame_custom_bottom_left = "."; user string ts_frame_custom_bottom_right = "."; user float ts_frame_scale = 1.0; user bool ts_frame_using_health_bar_colors = true;
OptionMenu ts_FrameOptions { Title "Frame options" Slider "Scale", ts_frame_scale, 0.1, 4.0, 0.1, 1 Option "Use health bar colors", ts_frame_using_health_bar_colors, OnOff StaticText "" Option "Custom characters", ts_frame_custom_enabled, OnOff StaticText "" Slider "Style", ts_frame_selected, 0, 7, 1, 0, ts_frame_custom_enabled, 1 StaticText "" StaticText "Custom characters" TextField "Top left", ts_frame_custom_top_left, ts_frame_custom_enabled, 0 TextField "Top right", ts_frame_custom_top_right, ts_frame_custom_enabled, 0 TextField "Bottom left", ts_frame_custom_bottom_left, ts_frame_custom_enabled, 0 TextField "Bottom right", ts_frame_custom_bottom_right, ts_frame_custom_enabled, 0 }
4.4.4. Crosshair
tsView_Crosshair = "Crosshair";
class tsView_Crosshair : Actor { static ui void draw(Array<ts_InformationPiece> informationPieces, double alpha, RenderEvent event, vector3 targetPosition, vector2 targetSize) { ts_Options options = ts_EventHandler.getInstance().getOptions(); if (options.getCrosshairAlways()) alpha = 1.0; else if (alpha <= 0.0) return; Font aFont = Font.findFont("NewSmallFont"); double scale = options.getCrosshairScale(); double halfLineHeight = aFont.getHeight() * scale / 2; vector2 center = (Screen.getWidth() / 2, options.getCrosshairY()); double height = options.getCrosshairHeight(); vector2 positions[3] = { (0, -halfLineHeight) + center - (0, height), (0, -halfLineHeight) + center, (0, -halfLineHeight) + center + (0, height) }; string parts[3] = { options.getCrosshairTop(), options.getCrosshairCenter(), options.getCrosshairBottom() }; int color = options.getCrosshairHealth() ? ts_Utils.getHealthColor(options, informationPieces) : Font.CR_Gray; for (int i = 0; i < 3; ++i) { string part = parts[i]; double width = aFont.stringWidth(part) * scale; ts_ViewUtils.drawText(aFont, part, width, positions[i], alpha, scale, color); } } }
double getCrosshairHeight() const { return mCrosshairHeight.getFloat(); } double getCrosshairScale() const { return mCrosshairScale.getFloat(); } string getCrosshairTop() const { return mCrosshairTop .getString(); } string getCrosshairCenter() const { return mCrosshairCenter.getString(); } string getCrosshairBottom() const { return mCrosshairBottom.getString(); } bool getCrosshairAlways() const { return mCrosshairAlways.getInt(); } bool getCrosshairHealth() const { return mCrosshairHealth.getInt(); } double getCrosshairY() const { // TODO: cache this Cvar. Cannot be cached like others because // it doesn't exist yet if loaded before PreciseCrosshair. Cvar crosshairY = Cvar.getCvar("pc_y", players[consolePlayer]); if (crosshairY) return crosshairY.getFloat(); let [x, y, width, height] = Screen.getViewWindow(); return y + height / 2; }
private Cvar mCrosshairHeight; private Cvar mCrosshairScale; private Cvar mCrosshairTop; private Cvar mCrosshairCenter; private Cvar mCrosshairBottom; private Cvar mCrosshairAlways; private Cvar mCrosshairHealth;
mCrosshairHeight = ts_ExistingCvar.find("ts_crosshair_height"); mCrosshairScale = ts_ExistingCvar.find("ts_crosshair_scale"); mCrosshairTop = ts_ExistingCvar.find("ts_crosshair_top"); mCrosshairCenter = ts_ExistingCvar.find("ts_crosshair_center"); mCrosshairBottom = ts_ExistingCvar.find("ts_crosshair_bottom"); mCrosshairAlways = ts_ExistingCvar.find("ts_crosshair_always"); mCrosshairHealth = ts_ExistingCvar.find("ts_crosshair_health");
user float ts_crosshair_height = 10.0; user float ts_crosshair_scale = 1.0; user string ts_crosshair_top = "v"; user string ts_crosshair_center = "> <"; user string ts_crosshair_bottom = "^"; user bool ts_crosshair_always = false; user bool ts_crosshair_health = true;
OptionMenu ts_CrosshairOptions { Title "Crosshair options" Option "Always enabled", ts_crosshair_always, OnOff Option "Use health bar colors", ts_crosshair_health, OnOff StaticText "" Slider "Height", ts_crosshair_height, 0.5, 40, 0.1 Slider "Scale", ts_crosshair_scale, 0.5, 4, 0.1 StaticText "" TextField "Top", ts_crosshair_top TextField "Center", ts_crosshair_center TextField "Bottom", ts_crosshair_bottom }
4.5. Core
4.5.1. Event handler
class ts_EventHandler : StaticEventHandler { override void onRegister() { (mOptions = new("ts_Options")).initialize(); (mBlocklist = new("ts_ClassList")).initialize("ts_blocklist"); (mFollowers = new("ts_ClassList")).initialize("ts_followers"); (mFader = new("ts_Fader")).initialize(); (mProjector = new("ts_Projector")).initialize(); mCurrentTarget = new("ts_TargetSource"); mDefaultFont = Font.findFont("NewSmallFont"); mDisabledFindersCvar = ts_ExistingCvar.find("ts_disabled_finders"); mDisabledFiltersCvar = ts_ExistingCvar.find("ts_disabled_filters"); mDisabledGettersCvar = ts_ExistingCvar.find("ts_disabled_getters"); mDisabledViewsCvar = ts_ExistingCvar.find("ts_disabled_views"); (mTextInterpolator = new("ts_TargetScreenPositionInterpolator")).initialize(mCurrentTarget); (mFrameMemory = new("ts_FrameMemory")).initialize(mCurrentTarget); collectConfigurations(); collectClasses(); } override void worldTick() { if (players[consolePlayer].mo == NULL) return; if (level.mapName ~== "titlemap") return; mProjector.prepare(); mFader.tick(); if (automapactive || !mOptions.isOn()) { mCurrentTarget.target = NULL; return; } findTargets(); filterTargets(); chooseCurrentTarget(); collectInformation(); } override void worldLoaded(WorldEvent event) { clearInformation(); } override void renderOverlay(RenderEvent event) { if (players[consolePlayer].mo == NULL) return; double alpha = mFader.getAlpha(event.fracTic); Array<string> disabledClasses; mDisabledViewsCvar.getString().split(disabledClasses, ",", TOK_SkipEmpty); applyConfiguration("tsView_", disabledClasses); int disabledClassesSize = disabledClasses.size(); int viewsCount = mViews.size(); for (int i = 0; i < viewsCount; ++i) { bool isEnabled = disabledClasses.find(mViewClasses[i]) == disabledClassesSize; if (!isEnabled) continue; mViews[i]. call(mInformationPieces, alpha, event, mLastTargetPosition, mLastTargetSize); } } override void networkProcess(ConsoleEvent event) { if (event.player == consolePlayer && event.name == "ts_read_getters") collectGetters(); } static clearscope ts_EventHandler getInstance() { return (ts_EventHandler)(StaticEventHandler.find("ts_EventHandler")); } clearscope void getFinders(out Array<string> result) { result.copy(mFinderClasses); } clearscope void getFilters(out Array<string> result) { result.copy(mFilterClasses); } clearscope void getGetters(out Array<string> result) { result.copy(mGetterClasses); } clearscope void getViews(out Array<string> result) { result.copy(mViewClasses); } clearscope ts_Options getOptions() { return mOptions; } clearscope ts_Projector getProjector() { return mProjector; } ts_ClassList getBlocklist() { return mBlocklist; } ui vector2 interpolateTextScreenPosition(vector2 position) { return mTextInterpolator.interpolateScreenPosition(position); } private void findTargets() { Array<string> disabledClasses; mDisabledFindersCvar.getString().split(disabledClasses, ",", TOK_SkipEmpty); applyConfiguration("tsFinder_", disabledClasses); int disabledClassesSize = disabledClasses.size(); int findersCount = mFinders.size(); for (int i = 0; i < findersCount; ++i) { bool isEnabled = disabledClasses.find(mFinderClasses[i]) == disabledClassesSize; Actor target = isEnabled ? mFinders[i].call() : NULL; if (target != NULL && mFollowers.containsParent(target.getClass())) target = target.master; mTargets[i] = target; } } private void filterTargets() { int filtersCount = mFilters.size(); int targetsCount = mTargets.size(); Array<string> disabledClasses; mDisabledFiltersCvar.getString().split(disabledClasses, ",", TOK_SkipEmpty); applyConfiguration("tsFilter_", disabledClasses); int disabledClassesSize = disabledClasses.size(); for (int targetIndex = 0; targetIndex < targetsCount; ++targetIndex) { if (mTargets[targetIndex] == NULL) continue; for (int i = 0; i < filtersCount; ++i) { bool isEnabled = disabledClasses.find(mFilterClasses[i]) == disabledClassesSize; if (!isEnabled) continue; if (mFilters[i].call(mTargets[targetIndex])) continue; mTargets[targetIndex] = NULL; break; } } } private void chooseCurrentTarget() { Actor closestTarget = NULL; double minDistance = double.max; foreach (target : mTargets) { if (target == NULL) continue; double distance = distanceTo(target); if (distance < minDistance) { closestTarget = target; minDistance = distance; } } if (closestTarget != NULL) { mLastTargetSize = (closestTarget.radius, closestTarget.height); mLastTargetPosition = closestTarget.pos; } mCurrentTarget.target = closestTarget; } private void collectInformation() { if (mCurrentTarget.target == NULL) { if (mFader.getAlpha(1.0) <= 0.0) clearInformation(); return; } clearInformation(); mFader.reset(); Array<string> disabledClasses; mDisabledGettersCvar.getString().split(disabledClasses, ",", TOK_SkipEmpty); applyConfiguration("tsGetter_", disabledClasses); int disabledClassesSize = disabledClasses.size(); int gettersCount = mGetters.size(); for (int i = 0; i < gettersCount; ++i) { bool isEnabled = disabledClasses.find(mGetterClasses[i]) == disabledClassesSize; if (isEnabled) { mInformationPieces[i].id = mGetterClasses[i]; [mInformationPieces[i].kind, mInformationPieces[i].text, mInformationPieces[i].value, mInformationPieces[i].max] = mGetters[i].call(mCurrentTarget.target); mInformationPieces[i].font = mFontGetters[i] ? mFontGetters[i].call(mCurrentTarget.target) : mDefaultFont; } } } private void clearInformation() { int gettersCount = mGetters.size(); for (int i = 0; i < gettersCount; ++i) mInformationPieces[i].kind = ts_Kind.Empty; } private void collectGetters() { mGetters.clear(); mGetterClasses.clear(); Array<string> orderedGetters; ts_ExistingCvar.find("ts_getters_order").getString() .split(orderedGetters, ",", TOK_SkipEmpty); foreach (getter : orderedGetters) { string className = "tsGetter_" .. getter; Class<Actor> aClass = className; if (aClass != NULL && !tryAddGetter(aClass)) Console.printf("ERROR: %s: invalid tsGetter", className); } foreach (aClass : AllActorClasses) { string className = aClass.getClassName(); string shortName = ts_Classes.shorten(className); if (mGetterClasses.find(shortName) != mGetterClasses.size()) continue; if (className.indexOf("tsGetter_") != -1 && !tryAddGetter(aClass)) Console.printf("ERROR: %s: invalid tsGetter", className); } ts_Classes.warnTooLongList(mGetterClasses, "getters"); } private void collectClasses() { foreach (aClass : AllActorClasses) { string className = aClass.getClassName(); if (className.indexOf("tsFinder_") != -1 && !tryAddFinder(aClass)) Console.printf("ERROR: %s: invalid tsFinder", className); if (className.indexOf("tsFilter_") != -1 && !tryAddFilter(aClass)) Console.printf("ERROR: %s: invalid tsFilter", className); if (className.indexOf("tsView_") != -1 && !tryAddView(aClass)) Console.printf("ERROR: %s: invalid tsView", className); } ts_Classes.warnTooLongList(mFinderClasses, "finders"); ts_Classes.warnTooLongList(mFilterClasses, "filters"); ts_Classes.warnTooLongList(mViewClasses, "views"); collectGetters(); } private bool tryAddFinder(class<Actor> aClass) { // This class replaces something: skip, it is handled on replacee side. if (Actor.getReplacee(aClass) != aClass) return true; // If this class is replaced, use that replacement but keep the original name. string className = aClass.getClassName(); aClass = Actor.getReplacement(aClass); let finder = (Function<play Actor()>)(findFunction(aClass, "find")); if (finder != NULL) { mFinders.push(finder); mFinderClasses.push(ts_Classes.shorten(className)); mTargets.push(NULL); } return finder != NULL; } private bool tryAddFilter(class<Actor> aClass) { // This class replaces something: skip, it is handled on replacee side. if (Actor.getReplacee(aClass) != aClass) return true; // If this class is replaced, use that replacement but keep the original name. string className = aClass.getClassName(); aClass = Actor.getReplacement(aClass); let filter = (Function<play bool(Actor)>)(findFunction(aClass, "isAllowed")); if (filter != NULL) { mFilters.push(filter); mFilterClasses.push(ts_Classes.shorten(className)); } return filter != NULL; } private bool tryAddGetter(class<Actor> aClass) { // This class replaces something: skip, it is handled on replacee side. if (Actor.getReplacee(aClass) != aClass) return true; // If this class is replaced, use that replacement but keep the original name. string className = aClass.getClassName(); aClass = Actor.getReplacement(aClass); let getter = (Function<play int, string, int, int(Actor)>) (findFunction(aClass, "makeInformation")); if (getter != NULL) { mGetters.push(getter); mGetterClasses.push(ts_Classes.shorten(className)); mInformationPieces.push(new("ts_InformationPiece")); mFontGetters. push((Function<play Font(Actor)>)(findFunction(aClass, "getFont"))); } return getter != NULL; } private bool tryAddView(class<Actor> aClass) { // This class replaces something: skip, it is handled on replacee side. if (Actor.getReplacee(aClass) != aClass) return true; // If this class is replaced, use that replacement but keep the original name. string className = aClass.getClassName(); aClass = Actor.getReplacement(aClass); let view = (Function <ui void (Array<ts_InformationPiece>, double, RenderEvent, vector3, vector2)>) (findFunction(aClass, "draw")); if (view != NULL) { mViews.push(view); mViewClasses.push(ts_Classes.shorten(className)); } return view != NULL; } private static double distanceTo(Actor anActor) { return (anActor != NULL) ? players[consolePlayer].mo.distance3d(anActor) : 0; } private ts_Options mOptions; private Cvar mDisabledFindersCvar; private Cvar mDisabledFiltersCvar; private Cvar mDisabledGettersCvar; private Cvar mDisabledViewsCvar; private Array< Function<play Actor()> > mFinders; private Array<string> mFinderClasses; private Array<Actor> mTargets; private ts_TargetSource mCurrentTarget; private Array< Function<play bool(Actor)> > mFilters; private Array<string> mFilterClasses; private vector3 mLastTargetPosition; private vector2 mLastTargetSize; private Array< Function<play int, string, int, int(Actor)> > mGetters; private Array< Function<play Font(Actor)> > mFontGetters; private Array<string> mGetterClasses; private Array<ts_InformationPiece> mInformationPieces; private Array< Function <ui void(Array<ts_InformationPiece>, double, RenderEvent, vector3, vector2)> > mViews; private Array<string> mViewClasses; private Font mDefaultFont; private ts_ClassList mBlocklist; private ts_ClassList mFollowers; private ts_Fader mFader; private ts_Projector mProjector; private ts_TargetScreenPositionInterpolator mTextInterpolator; private ts_FrameMemory mFrameMemory; clearscope ts_FrameMemory getFrameMemory() { return mFrameMemory; } clearscope bool isInConfig(string className) { return mConfiguration.checkKey(className); } private void collectConfigurations() { string lumpName = "ts_config"; for (int i = Wads.findLump(lumpName); i != -1; i = Wads.findLump(lumpName, i + 1)) { let configuration = Dictionary.fromString(Wads.readLump(i)); let iterator = DictionaryIterator.create(configuration); while (iterator.next()) { string key = iterator.key(); string value = iterator.value().makeLower(); if (value == "on") mConfiguration.insert(key, 1); else if (value == "off") mConfiguration.insert(key, 0); else if (value == "user") mConfiguration.remove(key); } } } clearscope void applyConfiguration(string prefix, out Array<string> disabledClasses) { foreach (className, isEnabled : mConfiguration) { if (className.left(prefix.length()) != prefix) continue; string shortClassName = ts_Classes.shorten(className); int index = disabledClasses.find(shortClassName); if (isEnabled) { if (index != disabledClasses.size()) disabledClasses.delete(index); } else { if (index == disabledClasses.size()) disabledClasses.push(shortClassName); } } } private Map<string, int> mConfiguration; }
4.5.2. Turn on/off
bool isOn() const { return mIsOn.getInt(); }
private Cvar mIsOn;
mIsOn = ts_ExistingCvar.find("ts_on");
user bool ts_on = true;
AddKeySection "$ts_Keys" ts_Keys Alias ts_turn_on "ts_on true" Alias ts_turn_off "ts_on false" Alias +ts_hold ts_turn_on Alias -ts_hold ts_turn_off AddMenuKey "$ts_Hold" +ts_hold AddMenuKey "$ts_On" ts_turn_on AddMenuKey "$ts_Off" ts_turn_off
ts_Keys = "TargeSpy keys"; ts_Hold = "Hold to turn on"; ts_On = "Turn on"; ts_Off = "Turn off";
4.5.3. Information piece
class ts_Kind { enum _ { Empty, PlainText, Value, ValueAndMax, Bar, } } class ts_InformationPiece { string id; // identifier for the getter that created this piece. int kind; // see ts_Kind. string text; // defined only for ts_Kind.PlainText. int value; // defined for ts_Kind.Value, ts_Kind.ValueAndMax, and ts_Kind.Bar. int max; // defined for ts_Kind.ValueAndMax and ts_Kind.Bar. Font font; // font for this piece. }
4.5.4. Class list
class ts_ClassList { void initialize(string lumpName) { for (int i = Wads.findLump(lumpName); i != -1; i = Wads.findLump(lumpName, i + 1)) { string contents = Wads.readLump(i); contents.replace("\r", ""); Array<string> lines; contents.split(lines, "\n", TOK_SkipEmpty); int linesCount = lines.size(); for (int l = 0; l < linesCount; ++l) { mClasses.insert(lines[l].makeLower(), 0); } } } bool contains(string className) const { return mClasses.checkKey(className.makeLower()); } bool containsParent(class<Object> aClass) { for (class<Object> parent = aClass; parent.getClassName() != "Thinker"; parent = parent.getParentClass()) { if (contains(parent.getClassName())) return true; } return false; } Map<string, int> mClasses; }
4.5.5. Existing Cvar
TODO: move to a module?
class ts_ExistingCvar { static Cvar find(Name name) { Cvar result = Cvar.findCvar(name); if (result == NULL) throwAbortException("ERROR: TargetSpy: %s Cvar not found.", name); return result; } }
4.5.6. Utilities
Functions in ts_Utils class.
getActorMaxHealth
static play int getActorMaxHealth(Actor anActor) { if (anActor.player != NULL && anActor.player.mo != NULL) return anActor.player.mo.getMaxHealth(); int result = anActor.spawnHealth(); string drpgToken = "DRPGMonsterStatsHandler"; bool isDRPG = (anActor.countInv(drpgToken) > 0); if (isDRPG) result = anActor.ACS_scriptCall("GetMonsterHealthMax"); string legendaryToken = "LDLegendaryMonsterToken"; bool isLegenDoom = (anActor.countInv(legendaryToken) > 0); if (isLegenDoom && !isDRPG) result *= Cvar.getCvar("LD_legendaryHealth").getInt() / 100; return result; }
getColor
static ui int getHealthColor(ts_Options options, Array<ts_InformationPiece> pieces) { double value; double max; bool found = false; foreach (piece : pieces) { if (piece.id == "SilentHealth") { found = true; value = piece.value; max = piece.max; break; } } if (!found) return Font.CR_Gray; ts_BarOptions healthBarOptions = options.getBarOptions("HealthBar"); int percent = int(round(value * 100 / max)); if (percent <= 0) return healthBarOptions.getEmptyColor(percent); return healthBarOptions.getFilledColor(percent); }
4.5.7. Target screen position interpolator
class ts_TargetSource { Actor target; } class ts_TargetScreenPositionInterpolator { void initialize(ts_TargetSource targetSource) { mTargetSource = targetSource; } ui vector2 interpolateScreenPosition(vector2 screenPosition) { Object currentTarget = mTargetSource.target; if (mLastInterpolatedTarget != currentTarget) { mLastInterpolatedTarget = currentTarget; mOldScreenPosition = screenPosition; return screenPosition; } vector2 diff = (screenPosition - mOldScreenPosition) / 2; mOldScreenPosition.x += round(diff.x); mOldScreenPosition.y += round(diff.y); return mOldScreenPosition; } private ts_TargetSource mTargetSource; private ui vector2 mOldScreenPosition; private ui Object mLastInterpolatedTarget; }
4.5.8. Class utilities
class ts_Classes { static void warnTooLongList(out Array<string> classes, string listName) { string disabledClasses = ts_su.join(classes, ","); if (disabledClasses.length() > 256) Console.printf("WARNING: %s list may be too long (%d) for a user cvar:\n%s", listName, disabledClasses.length(), disabledClasses); } static string shorten(string className) { return className.mid(className.indexOf("_") + 1); } }
4.5.9. Follower classes
???
HeadshotTargetZombie HeadshotTargetEliteZombie HeadshotTargetImp HeadshotTargetAngryImp HeadshotTargetNoble HeadshotTargetEliteNoble HeadshotTargetBossNoble HeadshotTargetArchvile HeadshotTargetEliteArchvile HeadshotTargetRevenant HeadshotTargetEliteRevenant HeadshotTargetMancubus HeadshotTargetSoulCommander HeadshotTargetCacodemon HeadshotTargetPainElemental HeadshotTargetDemon HeadshotTargetCyberdemon
Brutal Doom
HeadshotTargetBase
Venturous
HeadshotHitbox HeadshotHitboxDouble HeadshotHitboxHigher HeadshotHitboxHeavyeadshotHitbox
4.5.10. Projector
class ts_Projector { void initialize() { mGlProjection = new("ts_GlScreen"); mSwProjection = new("ts_SwScreen"); mCvarRenderer = Cvar.findCvar("vid_rendermode"); if (mCvarRenderer == NULL) Console.printf("TargetSpy: WARNING: vid_rendermode CVar not found"); } void prepare() { // Projection type detection is not quite right here. // vid_rendermode contains more complex information. // If something breaks, look here. if (mCvarRenderer != NULL) { int renderer = mCvarRenderer.getInt(); mProjection = (renderer == 0 || renderer == 1) ? (ts_ProjScreen)(mSwProjection) : (ts_ProjScreen)(mGlProjection); return; } mProjection = mGlProjection; } ui vector2 makePosition(RenderEvent event, vector3 worldPosition) const { PlayerInfo player = players[consolePlayer]; mProjection.cacheResolution(); mProjection.cacheFov(player.fov); mProjection.orientForRenderOverlay(event); mProjection.beginProjection(); mProjection.projectWorldPos(worldPosition); ts_Viewport viewport; viewport.fromHud(); return viewport.sceneToWindow(mProjection.projectToNormal()); } private ts_GlScreen mGlProjection; private ts_SwScreen mSwProjection; private CVar mCvarRenderer; private ts_ProjScreen mProjection; }
4.5.11. View utilities
class ts_ViewUtils ui { static void drawText(Font aFont, string text, double textWidth, vector2 position, double alpha, double scale, int color) { position.x -= textWidth / 2; Screen.drawText(aFont, color, int(round(position.x)), int(round(position.y)), text, DTA_ScaleX, scale, DTA_ScaleY, scale, DTA_Alpha, alpha); } static void drawBackground(double textWidth, double textHeight, vector2 position, double alpha) { position.x -= textWidth / 2; Screen.dim(0x000000, alpha / 2, int(round(position.x)), int(round(position.y)), int(round(textwidth)), int(round(textheight))); } static string toText(ts_InformationPiece piece, ts_Options options) { switch (piece.kind) { case ts_Kind.PlainText: return piece.text; case ts_Kind.Value: return string.format("%d", piece.value); case ts_Kind.ValueAndMax: return string.format("%d/%d", piece.value, piece.max); case ts_Kind.Bar: return makeBar(piece.value, piece.max, options.getBarOptions(piece.id)); } return ""; } static string makeBar(int value, int max, ts_BarOptions options) { int length = max(1, int(round(log(max) * 2))); int filledCount = 0; if (max < 1) { filledCount = 0; max = 1; } else { filledCount = clamp(int(ceil(double(value) * length / max)), 0, length); } int percent = int(round(double(value) * 100 / max)); int filledColor = options.getFilledColor(percent); int emptyColor = options.getEmptyColor(percent); string filled = string.format("%c", options.getFilledCharacter()); string empty = string.format("%c", options.getEmptyCharacter()); return string.format("\c%c%s\c%c%s", SMALL_A + filledColor, ts_su.repeat(filled, filledCount), SMALL_A + emptyColor, ts_su.repeat(empty, length - filledCount)); } const SMALL_A = ts_Ascii.LATIN_SMALL_LETTER_A; }
drawText
static void drawText(Font aFont, string text, double textWidth, vector2 position, double alpha, double scale, int color) { position.x -= textWidth / 2; Screen.drawText(aFont, color, int(round(position.x)), int(round(position.y)), text, DTA_ScaleX, scale, DTA_ScaleY, scale, DTA_Alpha, alpha); }
drawBackground
static void drawBackground(double textWidth, double textHeight, vector2 position, double alpha) { position.x -= textWidth / 2; Screen.dim(0x000000, alpha / 2, int(round(position.x)), int(round(position.y)), int(round(textwidth)), int(round(textheight))); } static string toText(ts_InformationPiece piece, ts_Options options) { switch (piece.kind) { case ts_Kind.PlainText: return piece.text; case ts_Kind.Value: return string.format("%d", piece.value); case ts_Kind.ValueAndMax: return string.format("%d/%d", piece.value, piece.max); case ts_Kind.Bar: return makeBar(piece.value, piece.max, options.getBarOptions(piece.id)); } return ""; }
makeBar
static string makeBar(int value, int max, ts_BarOptions options) { int length = max(1, int(round(log(max) * 2))); int filledCount = 0; if (max < 1) { filledCount = 0; max = 1; } else { filledCount = clamp(int(ceil(double(value) * length / max)), 0, length); } int percent = int(round(double(value) * 100 / max)); int filledColor = options.getFilledColor(percent); int emptyColor = options.getEmptyColor(percent); string filled = string.format("%c", options.getFilledCharacter()); string empty = string.format("%c", options.getEmptyCharacter()); return string.format("\c%c%s\c%c%s", SMALL_A + filledColor, ts_su.repeat(filled, filledCount), SMALL_A + emptyColor, ts_su.repeat(empty, length - filledCount)); } const SMALL_A = ts_Ascii.LATIN_SMALL_LETTER_A;
class ts_BarOptions { ts_BarOptions initialize(string tag) { mFilledColorCvars.push(findCvar(tag, "filled_25_color")); mFilledColorCvars.push(findCvar(tag, "filled_50_color")); mFilledColorCvars.push(findCvar(tag, "filled_75_color")); mFilledColorCvars.push(findCvar(tag, "filled_100_color")); mFilledColorCvars.push(findCvar(tag, "filled_101_color")); mEmptyColorCvars.push(findCvar(tag, "empty_0_color")); mEmptyColorCvars.push(findCvar(tag, "empty_25_color")); mEmptyColorCvars.push(findCvar(tag, "empty_50_color")); mEmptyColorCvars.push(findCvar(tag, "empty_75_color")); mEmptyColorCvars.push(findCvar(tag, "empty_100_color")); mFilledCharacterIndex = findCvar(tag, "filled_character_index"); mEmptyCharacterIndex = findCvar(tag, "empty_character_index"); mCharactersCvar = findCvar(tag, "characters"); return self; } private Cvar findCvar(string tag, string name) { let fullName = "ts_" .. tag .. "_" .. name; let aCvar = Cvar.findCvar(fullName); return (aCvar != NULL) ? aCvar : ts_ExistingCvar.find("ts_DefaultBar_" .. name); } int getFilledColor(int percent) const { return mFilledColorCvars[clamp(percent + 24, 25, 125) / 25 - 1].getInt(); } int getEmptyColor(int percent) const { return mEmptyColorCvars[clamp(percent + 24, 0, 100) / 25].getInt(); } int getFilledCharacter() const { return ts_su.getCodePointAt(mCharactersCvar.getString(), max(mFilledCharacterIndex.getInt(), 0)); } int getEmptyCharacter() const { return ts_su.getCodePointAt(mCharactersCvar.getString(), max(mEmptyCharacterIndex.getInt(), 0)); } private Array<Cvar> mFilledColorCvars; private Array<Cvar> mEmptyColorCvars; private Cvar mFilledCharacterIndex; private Cvar mEmptyCharacterIndex; private Cvar mCharactersCvar; }
ts_BarOptions getBarOptions(string aClass) { return mBarOptions.get(aClass); } private static void findGetters(out Array<string> result) { foreach (aClass : AllActorClasses) { string className = aClass.getClassName(); if (className.indexOf("tsGetter_") != -1) result.push(ts_Classes.shorten(className)); } }
Map<string, ts_BarOptions> mBarOptions;
mBarOptions.insert("DefaultBar", (new("ts_BarOptions")).initialize("DefaultBar")); Array<string> getterClasses; findGetters(getterClasses); foreach (aClass : getterClasses) mBarOptions.insert(aClass, (new("ts_BarOptions")).initialize(aClass));
user int ts_DefaultBar_filled_25_color = 3; // green user int ts_DefaultBar_filled_50_color = 3; user int ts_DefaultBar_filled_75_color = 3; user int ts_DefaultBar_filled_100_color = 3; user int ts_DefaultBar_filled_101_color = 3; user int ts_DefaultBar_empty_0_color = 6; // red user int ts_DefaultBar_empty_25_color = 6; user int ts_DefaultBar_empty_50_color = 6; user int ts_DefaultBar_empty_75_color = 6; user int ts_DefaultBar_empty_100_color = 6; user int ts_DefaultBar_filled_character_index = 0; user int ts_DefaultBar_empty_character_index = 0; user string ts_DefaultBar_characters = "█▓▒░❤♡☻☹⛧⛤☣✚⛊●☐☒☑⚙"; user int ts_HealthBar_filled_25_color = 3; // green user int ts_HealthBar_filled_50_color = 3; user int ts_HealthBar_filled_75_color = 3; user int ts_HealthBar_filled_100_color = 3; user int ts_HealthBar_filled_101_color = 3; user int ts_HealthBar_empty_0_color = 6; // red user int ts_HealthBar_empty_25_color = 6; user int ts_HealthBar_empty_50_color = 6; user int ts_HealthBar_empty_75_color = 6; user int ts_HealthBar_empty_100_color = 6; user int ts_HealthBar_filled_character_index = 0; user int ts_HealthBar_empty_character_index = 0; user string ts_HealthBar_characters = "█▓▒░❤♡☻☹⛧⛤☣✚⛊●☐☒☑⚙"; user int ts_ArmorBar_filled_25_color = 3; // green user int ts_ArmorBar_filled_50_color = 3; user int ts_ArmorBar_filled_75_color = 3; user int ts_ArmorBar_filled_100_color = 3; user int ts_ArmorBar_filled_101_color = 3; user int ts_ArmorBar_empty_0_color = 6; // red user int ts_ArmorBar_empty_25_color = 6; user int ts_ArmorBar_empty_50_color = 6; user int ts_ArmorBar_empty_75_color = 6; user int ts_ArmorBar_empty_100_color = 6; user int ts_ArmorBar_filled_character_index = 0; user int ts_ArmorBar_empty_character_index = 0; user string ts_ArmorBar_characters = "█▓▒░❤♡☻☹⛧⛤☣✚⛊●☐☒☑⚙";
OptionMenu ts_DefaultBarOptions { Title "DefaultBar options" Slider "Filled character", ts_DefaultBar_filled_character_index, 0, 99, 1, 0 ts_HighlightedCharacter ts_DefaultBar_filled_character_index, DefaultBar Slider "Empty character", ts_DefaultBar_empty_character_index, 0, 99, 1, 0 ts_HighlightedCharacter ts_DefaultBar_empty_character_index, DefaultBar TextField "Characters", ts_DefaultBar_characters StaticText "" ts_BarPreview 0, "DefaultBar" ts_BarPreview 25, "DefaultBar" ts_BarPreview 50, "DefaultBar" ts_BarPreview 75, "DefaultBar" ts_BarPreview 100, "DefaultBar" ts_BarPreview 101, "DefaultBar" StaticText "" StaticText "Colors for filled characters" Option "1-25%" , ts_DefaultBar_filled_25_color, TextColors Option "26-50%", ts_DefaultBar_filled_50_color, TextColors Option "51-75%", ts_DefaultBar_filled_75_color, TextColors Option "76-100%", ts_DefaultBar_filled_100_color, TextColors Option "> 100%", ts_DefaultBar_filled_101_color, TextColors StaticText "" StaticText "Colors for empty characters" Option "≤ 0", ts_DefaultBar_empty_0_color, TextColors Option "1-25%", ts_DefaultBar_empty_25_color, TextColors Option "26-50%", ts_DefaultBar_empty_50_color, TextColors Option "51-75%", ts_DefaultBar_empty_75_color, TextColors Option "76-100%", ts_DefaultBar_empty_100_color, TextColors } OptionMenu ts_HealthBarOptions { Title "HealthBar options" Slider "Filled character", ts_HealthBar_filled_character_index, 0, 99, 1, 0 ts_HighlightedCharacter ts_HealthBar_filled_character_index, HealthBar Slider "Empty character", ts_HealthBar_empty_character_index, 0, 99, 1, 0 ts_HighlightedCharacter ts_HealthBar_empty_character_index, HealthBar TextField "Characters", ts_HealthBar_characters StaticText "" ts_BarPreview 0, "HealthBar" ts_BarPreview 25, "HealthBar" ts_BarPreview 50, "HealthBar" ts_BarPreview 75, "HealthBar" ts_BarPreview 100, "HealthBar" ts_BarPreview 101, "HealthBar" StaticText "" StaticText "Colors for filled characters" Option "1-25%" , ts_HealthBar_filled_25_color, TextColors Option "26-50%", ts_HealthBar_filled_50_color, TextColors Option "51-75%", ts_HealthBar_filled_75_color, TextColors Option "76-100%", ts_HealthBar_filled_100_color, TextColors Option "> 100%", ts_HealthBar_filled_101_color, TextColors StaticText "" StaticText "Colors for empty characters" Option "≤ 0", ts_HealthBar_empty_0_color, TextColors Option "1-25%", ts_HealthBar_empty_25_color, TextColors Option "26-50%", ts_HealthBar_empty_50_color, TextColors Option "51-75%", ts_HealthBar_empty_75_color, TextColors Option "76-100%", ts_HealthBar_empty_100_color, TextColors } OptionMenu ts_ArmorBarOptions { Title "ArmorBar options" Slider "Filled character", ts_ArmorBar_filled_character_index, 0, 99, 1, 0 ts_HighlightedCharacter ts_ArmorBar_filled_character_index, ArmorBar Slider "Empty character", ts_ArmorBar_empty_character_index, 0, 99, 1, 0 ts_HighlightedCharacter ts_ArmorBar_empty_character_index, ArmorBar TextField "Characters", ts_ArmorBar_characters StaticText "" ts_BarPreview 0, "ArmorBar" ts_BarPreview 25, "ArmorBar" ts_BarPreview 50, "ArmorBar" ts_BarPreview 75, "ArmorBar" ts_BarPreview 100, "ArmorBar" ts_BarPreview 101, "ArmorBar" StaticText "" StaticText "Colors for filled characters" Option "1-25%" , ts_ArmorBar_filled_25_color, TextColors Option "26-50%", ts_ArmorBar_filled_50_color, TextColors Option "51-75%", ts_ArmorBar_filled_75_color, TextColors Option "76-100%", ts_ArmorBar_filled_100_color, TextColors Option "> 100%", ts_ArmorBar_filled_101_color, TextColors StaticText "" StaticText "Colors for empty characters" Option "≤ 0", ts_ArmorBar_empty_0_color, TextColors Option "1-25%", ts_ArmorBar_empty_25_color, TextColors Option "26-50%", ts_ArmorBar_empty_50_color, TextColors Option "51-75%", ts_ArmorBar_empty_75_color, TextColors Option "76-100%", ts_ArmorBar_empty_100_color, TextColors }
4.5.12. Menu classes
- Toggles
user string ts_disabled_finders = "NonShootable,Noblockmap"; user string ts_disabled_filters = "LightLevel,Enemies,Dormant,Active"; user string ts_disabled_getters = "Health,ArmorBar,Class,Tag,Flags"; user string ts_disabled_views = "Frame,Crosshair";
ts_AllOn = "Activate all"; ts_AllOff = "Deactivate all";
class ts_ClassToggles : OptionMenu { void fill(out Array<string> classes, string classPrefix, string disabledClassesCvarName) { let eventHandler = ts_EventHandler.getInstance(); mDesc.mItems.clear(); int classesCount = classes.size(); for (int i = 0; i < classesCount; ++i) { if (eventHandler.isInConfig(classPrefix .. classes[i])) continue; let toggle = new("ts_ClassToggle"); toggle.initialize(classes[i], classPrefix, disabledClassesCvarName); mDesc.mItems.push(toggle); } mDesc.mItems.push(new("OptionMenuItemStaticText").initDirect("", 0)); mDesc.mItems.push(new("OptionMenuItemCommand") .init("$ts_AllOn", disabledClassesCvarName .. " \"\"")); mDesc.mItems.push(new("OptionMenuItemCommand") .init("$ts_AllOff", string.format("%s \"%s\"", disabledClassesCvarName, ts_su.join(classes, ",")))); mDesc.mSelectedItem = clamp(mDesc.mSelectedItem, 0, mDesc.mItems.size() - 1); } } class ts_ClassToggle : OptionMenuItemOptionBase { void initialize(string aClass, string classPrefix, string disabledClassesCvarName) { mClass = aClass; mCvar = ts_ExistingCvar.find(disabledClassesCvarName); Super.init("$" .. classPrefix .. aClass, '', "OnOff", NULL, 0); } override int getSelection() { Array<string> parts; mCvar.getString().split(parts, ",", TOK_SkipEmpty); return parts.find(mClass) == parts.size(); } override void setSelection(int selection) { Array<string> parts; mCvar.getString().split(parts, ",", TOK_SkipEmpty); int found = parts.find(mClass); if (selection == (found == parts.size())) return; if (selection) parts.delete(found); else parts.push(mClass); mCvar.setString(ts_su.join(parts, ",")); } private Cvar mCvar; private string mClass; } class tsFinder_Toggles : ts_ClassToggles { override void init(Menu parent, OptionMenuDescriptor descriptor) { Super.init(parent, descriptor); Array<string> classes; ts_EventHandler.getInstance().getFinders(classes); Super.fill(classes, "tsFinder_", "ts_disabled_finders"); } } class tsFilter_Toggles : ts_ClassToggles { override void init(Menu parent, OptionMenuDescriptor descriptor) { Super.init(parent, descriptor); Array<string> classes; ts_EventHandler.getInstance().getFilters(classes); Super.fill(classes, "tsFilter_", "ts_disabled_filters"); } } class tsGetter_Toggles : ts_ClassToggles { override void init(Menu parent, OptionMenuDescriptor descriptor) { Super.init(parent, descriptor); Array<string> classes; ts_EventHandler.getInstance().getGetters(classes); Super.fill(classes, "tsGetter_", "ts_disabled_getters"); } } class tsView_Toggles : ts_ClassToggles { override void init(Menu parent, OptionMenuDescriptor descriptor) { Super.init(parent, descriptor); Array<string> classes; ts_EventHandler.getInstance().getViews(classes); Super.fill(classes, "tsView_", "ts_disabled_views"); } }
- Highlighted character static text
class OptionMenuItemts_HighlightedCharacter : OptionMenuItemStaticText { OptionMenuItemStaticText init(Name command, string tag) { mCvar = ts_ExistingCvar.find(command); mCharactersCvar = ts_ExistingCvar.find(string.format("ts_%s_characters", tag)); return Super.init("", -1); } override int draw(OptionMenuDescriptor desc, int y, int indent, bool selected) { string characters = mCharactersCvar.getString(); mLabel = ts_su.highlight(characters, clamp(mCvar.getInt(), 0, characters.length() - 1), Font.CR_Teal); drawLabel(indent, y, Font.CR_White); return -1; } private Cvar mCvar; private Cvar mCharactersCvar; }
- Bar preview
class OptionMenuItemts_BarPreview : OptionMenuItemStaticText { OptionMenuItemStaticText init(int percent, string aClass) { mPercent = percent; mClass = aClass; return Super.init("", -1); } override int draw(OptionMenuDescriptor desc, int y, int indent, bool selected) { ts_BarOptions options = ts_EventHandler.getInstance().getOptions().getBarOptions(mClass); int value = int(round(double(mPercent) * 64 / 100)); mLabel = string.format("%3d%% ", mPercent) .. ts_ViewUtils.makeBar(value, 64, options); drawLabel(indent, y, Font.CR_White); return -1; } private int mPercent; private string mClass; }
- Getters order
OptionMenu ts_GettersOrder { Class ts_GettersOrderMenu Title "Getters order" }
user string ts_getters_order = "";
class ts_GettersOrderMenu : OptionMenu { override void init(Menu parent, OptionMenuDescriptor descriptor) { Super.init(parent, descriptor); ts_EventHandler eventHandler = ts_EventHandler.getInstance(); eventHandler.getGetters(mClasses); eventHandler.applyConfiguration("tsGetter_", mClasses); mDesc.mItems.clear(); int classesCount = mClasses.size(); for (int i = 0; i < classesCount; ++i) { let entry = new("ts_GettersOrderElement"); entry.init("$tsGetter_" .. mClasses[i], i, self); mDesc.mItems.push(entry); } mDesc.mSelectedItem = clamp(mDesc.mSelectedItem, 0, mDesc.mItems.size() - 1); mOrderCvar = ts_ExistingCvar.find("ts_getters_order"); } void moveDown(int i) { if (i + 1 >= mDesc.mItems.size()) return; { let temp = mClasses[i]; mClasses[i] = mClasses[i + 1]; mClasses[i + 1] = temp; } { let temp = mDesc.mItems[i]; mDesc.mItems[i] = mDesc.mItems[i + 1]; mDesc.mItems[i + 1] = temp; } ++mDesc.mSelectedItem; (ts_GettersOrderElement)(mDesc.mItems[i]).setIndex(i); (ts_GettersOrderElement)(mDesc.mItems[i + 1]).setIndex(i + 1); mOrderCvar.setString(ts_su.join(mClasses, ",")); EventHandler.sendNetworkEvent("ts_read_getters"); } Array<string> mClasses; Cvar mOrderCvar; }
class ts_GettersOrderElement : OptionMenuItemCommand { ts_GettersOrderElement init(string label, int index, ts_GettersOrderMenu menu) { Super.init(label, ""); mIndex = index; mMenu = menu; return self; } override bool activate() { mMenu.moveDown(mIndex); return true; } void setIndex(int index) { mIndex = index; } private int mIndex; private ts_GettersOrderMenu mMenu; }
4.5.13. Configuration
Configuration is a way to customize how TargetSpy processes finders, filters, getters and views. By default these classes are enabled or disabled by user choice. Configuration can force them to be on or off. If a class is forced on, it is always active and hidden from option menus. If a class is forced off, it is always disabled and hidden from option menus.
Several configurations may be loaded at once. If more than one configuration configures a class, the last loaded configuration wins.
Configuration lump has name ts_config and is in JSON object format. Key is full
class name, value is "off", "on", or "user". "user" can be used to override previous
configuration.
Example:
class tsFilter_Abc : Actor { static bool isAllowed(Actor anActor) { return true; } } class tsGetter_Def : Actor { static int, string, int, int makeInformation(Actor anActor) { return ts_Kind.PlainText, "Def", 0, 0; } }
{
"tsFilter_Abc": "on",
"tsGetter_Def": "off",
"tsGetter_Health": "user"
}
- Implementation
collectConfigurations();clearscope bool isInConfig(string className) { return mConfiguration.checkKey(className); } private void collectConfigurations() { string lumpName = "ts_config"; for (int i = Wads.findLump(lumpName); i != -1; i = Wads.findLump(lumpName, i + 1)) { let configuration = Dictionary.fromString(Wads.readLump(i)); let iterator = DictionaryIterator.create(configuration); while (iterator.next()) { string key = iterator.key(); string value = iterator.value().makeLower(); if (value == "on") mConfiguration.insert(key, 1); else if (value == "off") mConfiguration.insert(key, 0); else if (value == "user") mConfiguration.remove(key); } } } clearscope void applyConfiguration(string prefix, out Array<string> disabledClasses) { foreach (className, isEnabled : mConfiguration) { if (className.left(prefix.length()) != prefix) continue; string shortClassName = ts_Classes.shorten(className); int index = disabledClasses.find(shortClassName); if (isEnabled) { if (index != disabledClasses.size()) disabledClasses.delete(index); } else { if (index == disabledClasses.size()) disabledClasses.push(shortClassName); } } } private Map<string, int> mConfiguration;
4.5.14. Extension replacements
Use either replaces keyword:
class tsGetter_TestClass : Actor replaces tsGetter_Class { static int, string, int, int makeInformation(Actor anActor) { let [kind, text, value, max] = tsGetter_Class.makeInformation(anActor); return kind, text .. " - class test", value, max; } }
Or replace classes with an EventHandler:
class tsGetter_TestTag : Actor { static int, string, int, int makeInformation(Actor anActor) { let [kind, text, value, max] = tsGetter_Tag.makeInformation(anActor); return kind, text .. " - name test", value, max; } } class tst_EventHandler : StaticEventHandler { override void CheckReplacement(ReplaceEvent event) { if (event.replacee.getClassName() == "tsGetter_Tag") event.replacement = "tsGetter_TestTag"; } }
GameInfo
{
EventHandlers = "tst_EventHandler"
}
Optional: replace user-visible class name with a new one:
[enu default] tsGetter_Class = "Class name test";
5. Project setup
GameInfo
{
EventHandlers = "ts_EventHandler"
}
version 4.14.3 #include "zscript/ts_common.zs" #include "zscript/ts_event_handler.zs" #include "zscript/ts_filters.zs" #include "zscript/ts_finders.zs" #include "zscript/ts_getters.zs" #include "zscript/ts_menus.zs" #include "zscript/ts_options.zs" #include "zscript/ts_types.zs" #include "zscript/ts_views.zs" #include "zscript/ts_PlainTranslator.zs" #include "zscript/ts_StringUtils.zs" #include "zscript/ts_libeye.zs"
version 4.14.3
class tsFilter_Abc : Actor
{
static bool isAllowed(Actor anActor)
{
return true;
}
}
class tsGetter_Def : Actor
{
static int, string, int, int makeInformation(Actor anActor)
{
return ts_Kind.PlainText, "Def", 0, 0;
}
}
class tsGetter_TestClass : Actor replaces tsGetter_Class
{
static int, string, int, int makeInformation(Actor anActor)
{
let [kind, text, value, max] = tsGetter_Class.makeInformation(anActor);
return kind, text .. " - class test", value, max;
}
}
class tsGetter_TestTag : Actor
{
static int, string, int, int makeInformation(Actor anActor)
{
let [kind, text, value, max] = tsGetter_Tag.makeInformation(anActor);
return kind, text .. " - name test", value, max;
}
}
class tst_EventHandler : StaticEventHandler
{
override void CheckReplacement(ReplaceEvent event)
{
if (event.replacee.getClassName() == "tsGetter_Tag")
event.replacement = "tsGetter_TestTag";
}
}
Translatable strings:
[enu default] ts_OptionsTitle = "TargetSpy \cd⌖"; tsFinder_Aim = "Aim target"; tsFinder_Shootable = "Shootable only"; tsFinder_NonShootable = "Non-shootable too"; tsFinder_Noblockmap = "Everything"; tsFilter_Blocklist = "Blocklist"; tsFilter_Monsters = "Monsters only"; tsFilter_LightLevel = "Lit only"; tsFilter_Health = "Positive health only"; tsFilter_Hidden = "Not hidden only"; tsFilter_Enemies = "Enemies only"; tsFilter_Dormant = "Not dormant only"; tsFilter_Active = "Active only"; tsGetter_Health = "Health"; tsGetter_HealthAndMax = "Health and max health"; tsGetter_HealthBar = "Health bar"; tsGetter_ArmorBar = "Armor bar"; tsGetter_Class = "Class name"; tsGetter_Tag = "Tag"; tsGetter_TagAndClass = "Tag (class)"; tsGetter_Flags = "Flags"; ts_Friendly = "Friendly"; ts_Invulnerable = "Invulnerable"; ts_Boss = "Boss"; ts_Dormant = "Dormant"; ts_Buddha = "Buddha"; ts_Undamageable = "Undamageable"; ts_NoBlockmap = "No blockmap"; tsView_Text = "Plain text"; tsView_Frame = "Frame"; tsView_Crosshair = "Crosshair"; ts_Keys = "TargeSpy keys"; ts_Hold = "Hold to turn on"; ts_On = "Turn on"; ts_Off = "Turn off"; ts_AllOn = "Activate all"; ts_AllOff = "Deactivate all"; [ru] tsGetter_Health = "Здоровье"; ts_Boss = "Босс"; ts_Toggles = "Переключатели";
class ts_Options { void initialize() { mMinLightLevelCvar = ts_ExistingCvar.find("ts_min_light_level"); mScaleCvar = ts_ExistingCvar.find("ts_scale"); mXPositionCvar = ts_ExistingCvar.find("ts_x_position"); mYPositionCvar = ts_ExistingCvar.find("ts_y_position"); mPlaceCvar = ts_ExistingCvar.find("ts_place"); mBackgroundCvar = ts_ExistingCvar.find("ts_background"); mHealthColors = ts_ExistingCvar.find("ts_using_health_bar_colors"); mCrosshairHeight = ts_ExistingCvar.find("ts_crosshair_height"); mCrosshairScale = ts_ExistingCvar.find("ts_crosshair_scale"); mCrosshairTop = ts_ExistingCvar.find("ts_crosshair_top"); mCrosshairCenter = ts_ExistingCvar.find("ts_crosshair_center"); mCrosshairBottom = ts_ExistingCvar.find("ts_crosshair_bottom"); mCrosshairAlways = ts_ExistingCvar.find("ts_crosshair_always"); mCrosshairHealth = ts_ExistingCvar.find("ts_crosshair_health"); mIsOn = ts_ExistingCvar.find("ts_on"); mBarOptions.insert("DefaultBar", (new("ts_BarOptions")).initialize("DefaultBar")); Array<string> getterClasses; findGetters(getterClasses); foreach (aClass : getterClasses) mBarOptions.insert(aClass, (new("ts_BarOptions")).initialize(aClass)); } int getMinLightLevel() const { return mMinLightLevelCvar.getInt(); } double getScale() const { return mScaleCvar.getFloat(); } double getXPosition() const { return mXPositionCvar.getFloat(); } double getYPosition() const { return mYPositionCvar.getFloat(); } enum Places { PlaceFixed, PlaceAboveTarget, PlaceBelowTarget } int getPlace() const { return mPlaceCvar.getInt(); } bool isBackgroundEnabled() const { return mBackgroundCvar.getInt(); } bool isUsingHealthBarColors() const { return mHealthColors.getInt(); } double getCrosshairHeight() const { return mCrosshairHeight.getFloat(); } double getCrosshairScale() const { return mCrosshairScale.getFloat(); } string getCrosshairTop() const { return mCrosshairTop .getString(); } string getCrosshairCenter() const { return mCrosshairCenter.getString(); } string getCrosshairBottom() const { return mCrosshairBottom.getString(); } bool getCrosshairAlways() const { return mCrosshairAlways.getInt(); } bool getCrosshairHealth() const { return mCrosshairHealth.getInt(); } double getCrosshairY() const { // TODO: cache this Cvar. Cannot be cached like others because // it doesn't exist yet if loaded before PreciseCrosshair. Cvar crosshairY = Cvar.getCvar("pc_y", players[consolePlayer]); if (crosshairY) return crosshairY.getFloat(); let [x, y, width, height] = Screen.getViewWindow(); return y + height / 2; } bool isOn() const { return mIsOn.getInt(); } ts_BarOptions getBarOptions(string aClass) { return mBarOptions.get(aClass); } private static void findGetters(out Array<string> result) { foreach (aClass : AllActorClasses) { string className = aClass.getClassName(); if (className.indexOf("tsGetter_") != -1) result.push(ts_Classes.shorten(className)); } } private Cvar mMinLightLevelCvar; private Cvar mScaleCvar; private Cvar mXPositionCvar; private Cvar mYPositionCvar; private Cvar mPlaceCvar; private Cvar mBackgroundCvar; private Cvar mHealthColors; private Cvar mCrosshairHeight; private Cvar mCrosshairScale; private Cvar mCrosshairTop; private Cvar mCrosshairCenter; private Cvar mCrosshairBottom; private Cvar mCrosshairAlways; private Cvar mCrosshairHealth; private Cvar mIsOn; Map<string, ts_BarOptions> mBarOptions; }
6. Tests
Check that the add-on at least can be loaded and doesn't error out on a target.
wait 2; map map01 wait 4; summon doomimp wait 8; quit