Gearbox
Table of Contents
- 1. About
- 2. License
- 3. Changelog
- 4. Option menus
- 5. Sounds
- 6. Keys
- 7. Source
- 7.1. Behavior
- 7.1.1. Activity
- 7.1.2. Changer
- 7.1.3. Custom weapon order storage
- 7.1.4. Event processor
- 7.1.5. Font selector
- 7.1.6. Freezer
- 7.1.7. Input
- 7.1.8. Inventory menu
- 7.1.9. Inventory user
- 7.1.10. Netevent processor
- 7.1.11. Options
- 7.1.12. Sender
- 7.1.13. Sounds
- 7.1.14. View model
- 7.1.15. Weapon data
- 7.1.16. Weapon data loader
- 7.1.17. Weapon menu
- 7.1.18. Action menu
- 7.2. Display
- 7.3. Engine
- 7.4. Event handler
- 7.4.1. World tick
- 7.4.2. Console process
- 7.4.3. Input process
- 7.4.4. Process input in weapons
- 7.4.5. Process input in inventory
- 7.4.6. Process input in actions
- 7.4.7. Process input in none
- 7.4.8. Network process
- 7.4.9. Render overlay
- 7.4.10. Get active view type
- 7.4.11. Is disabled on automap
- 7.4.12. Is player frozen
- 7.4.13. Tick if
- 7.4.14. Open weapons
- 7.4.15. Open inventory
- 7.4.16. Open actions
- 7.4.17. Is dead
- 7.4.18. Close
- 7.4.19. Confirm selection
- 7.4.20. Drop item
- 7.4.21. Rotate weapon priority
- 7.4.22. Rotate weapon slot
- 7.4.23. Reset custom order
- 7.4.24. Initialize
- 7.4.25. Variables
- 7.5. Service
- 7.6. Tools
- 7.7. Wheel
- 7.8. Wheel view
- 7.8.1. Constructor
- 7.8.2. Setters
- 7.8.3. Display
- 7.8.4. Draw outer weapons
- 7.8.5. Drawing wheels
- 7.8.6. Display weapon
- 7.8.7. Draw icon
- 7.8.8. Draw icon substitute
- 7.8.9. Draw ammo pip
- 7.8.10. Draw ammo
- 7.8.11. Draw quantity pips
- 7.8.12. Item angle
- 7.8.13. Draw hands
- 7.8.14. Draw weapon description
- 7.8.15. Draw pointer
- 7.8.16. Constants
- 7.8.17. Variables
- 7.1. Behavior
- 8. Test
{
"old_version": "0.8.1",
"depends": {
"Fonts": "gb_",
"MD5": "gb_",
"PreviousWeapon": "gb_",
"StringUtils": "gb_",
"VmAbortReporter": "gb_"
}
}
1. About
Gearbox provides more convenient ways to select weapons and items.
Gearbox is a part of DoomToolbox.
1.1. How to use
- open the menu by assigned key, or by next/previous weapon keys, if enabled in options
- select the weapon with next/previous weapon keys, or with mouse (wheel only)
- use the drop weapon or drop inventory keys to drop selected thing
1.2. Features
- Different representations: blocks, wheel, plain text
- Press Fire key to select and Alt Fire key to cancel
- Color and scale options
- Behavior options
- Multiplayer compatible
- Reaction to number keys
- extras.wad icon support for vanilla weapons
- Inventory item selection
- Ability to drop items or weapons from the menu
1.3. Note for weapon mod authors
If you want Gearbox to support your mod out of the box, assign Inventory.AltHudIcon
for your weapons! Tag property is also nice to have.
1.4. Known issues
- PyWeaponWheel v0.3 overrides time freezing. If you are using both mods and want to
freeze time with Gearbox, set PyWeaponWheel's option "Freeze when wheel is open"
(
py_weaponwheel_freezeCVar) to Off. Note that PyWeaponWheel may be built in some mods. The solution is the same: disable time PyWeaponWheel's time freezing. - Weapon icons in wheel aren't affected by "HUD preserves aspect ration" option.
- Mouse input in wheel in multiplayer causes screen shake.
- Cheat codes that contain keys assigned for weapon slots aren't supported as Gearbox blocks them.
- Freezing enemies when menu is opened doesn't play well with
freezeconsole command: everything may stay frozen.
1.5. Creating a soundpack
- Replace the
GB_PACKstring in language.txt with your soundpack's name, Replace the sndinfo definitions with your own sounds, for example:
In sndinfo.txt, add:
gearbox/pack/tick sounds/[soundpack name]_tick.ogggearbox/pack/open sounds/[soundpack name]_open.ogggearbox/pack/close sounds/[soundpack name]_close.ogggearbox/pack/nope sounds/[soundpack name]_nope.ogg
(replace [soundpack name] with your own name!)
- Select "Custom Soundpack" in the Gearbox UI options, and check if your sounds work.
2. License
SPDX-FileCopyrightText: © 2020 Alexander Kromm (m8f) <mmaulwurff@gmail.com> SPDX-FileCopyrightText: © 2022 generic-name-guy SPDX-FileCopyrightText: © 2022 Carrascado SPDX-FileCopyrightText: © 2025 SandPoot SPDX-License-Identifier: GPL-3.0-only
3. Changelog
- removed unused and missing sound definition
- moved to the new translation system
- better handle cases when weapons disappear
- make "select on release" option not affect inventory
- added options to choose view type for intercepted keys
- inventory menu now uses the engine item selection
v0.8.1
- expand "reverse cycling order" behavior
- set original weapons order as default
- fix blocks view max height calculation
v0.8.0
- add option to print selection
- fix weapon order to match original order
- use custom order even when not "intercepting"
- align menu icon
- remove prompts for safe commands
- fix warnings: replace uint with int
- adds dropping from the menu
v0.7.3
- fix VM Abort when
externalSelectedIndexInModelisn't set TextureIDtype fixes for GZDoom 4.14- fix opening the menu on single weapon
- fix VM abort when pending weapon is NULL
- add Brazilian Portuguese localization
v0.7.2
- don't open weapon menu if there is no weapons
- fix text out of weapon blocks
- replace ellipsis with three dots for fonts that don't have ellipsis
- feature/option select first slot weapon
- feature: option to reverse slot cycle order
- add audio feedback when menu can't be opened
- fix a bug by which the inventory was "invisibly" opened and closed when there were no items
- now there is a code-wise difference between the open and close menu sounds
- fix toggle and tick sounds played at the same time
v0.7.1
- fix a crash on Delta Touch with GLES 3.2 caused by blur shader
- add menu command to copy custom player color
- make custom weapon order keys descriptions more clear
- fix gitignore
- search for textures more precisely
v0.7.0
- don't scale weapons with no icons in wheel
- prevent weapon sway in wheel
- fix changing weapon order
- fix keyconf lump
- adapt to inverted mouse settings
- organize menus
- fix VM abort in inventory menu
- synchronize CVar lists
- add resetting custom weapon order
- add key to change weapon slot
- add key to change weapon priority
- add font selection
v0.6.0
- fix VM abort in text view when no weapons
- add plain text view
- fix: take into account weapon scaling properties for icons
- fix: SWWM GZ - two Explodium guns can be selected
- fix: velocity after freezing was restored even if freezing is off
- fix: unblock mouse input in Blocks view
- update build script
- add more null checks
v0.5.1
- add space after menu titles
- fix using inventory items when player is frozen
- fix wheel hands length
v0.5.0
- fix truncation of floating point warning
- update VM abort handler, and an option to disable it
- Revert "wheel: draw weapon tag if it has no icon"
- add wheel scale
- separate Screen class from blocks view, move to wheel
- remove redundant menu option
- move text drawing class to wheel
- display weapon name tags on change with native GZDoom function
- fix gravity after closing gearbox
- wheel: draw weapon tag if it has no icon
- add compatibility with Event Handler Destroyer
- add Gearbox options menu to simplified options menu
- don't call statusbar instance for static
getInventoryIconfunction - fix height when frozen in Heretic water
- move networking to sender
- add icon to options menu
- rework player and enemies freezing
- add KeksDose to Acknowledgments
- new input
- update VM abort handler
- fix blocks view positioning
v0.4.1
- hardcode icons for Smooth Doom
- prevent VM abort when no inventory items
- fix selecting hidden weapons
- make previous weapon carry over to the next level
- prevent blocks inventory view from aborting VM when there are no items
- don't draw placeholder icon in blocks view
- increase max multiwheel threshold, draw selected weapon on top
- add note about keys to Readme
- fix constant ticking on tomed weapons
- fix VM abort when the last inventory item is used up
- fix misaligned quantity dots when quantity is odd
- add missing CVars to VM abort handler and
gb_reset - fix truncation of floating point values
v0.4
- better handling of tall weapon icons
- don't rotate items in wheel, respect preserve aspect ration setting
- add an option to set if frozen player can open Gearbox
- move UI options to a submenu
- add reset to defaults command
- make position configurable for blocks view
- add blur option
- add a command for manual report
- add CVars dump to VM abort handler
- add VM abort handler
- add select previous weapon key from mgizmos
- fix complementary colors in blocks view
- fix division by zero when max ammo is 0
- don't draw 10 pips if ammo or number of items is less than 10
- wheel: don't move weapon description if not necessary
- fix repeated ticking when player has no weapons
- don't show amount for items with max amount 1
- add menudef entry for inventory menu key
- allow slot keys in inventory menu
- add tick sound to inventory menu
- fix next/previous buttons in inventory menu
- bare-bones inventory menu
v0.3.2
- increase minimal angle between wheel hands for clarity
- make wheel hands transparent in the middle
- draw the selected thing in the second wheel on top of other things
- remove a message for obsolete key bind
- fix out of bounds angle on the second wheel
- limit minimal angle between wheel hands
- let Gearbox know the current selected weapon
v0.3.1
- add missing people to Acknowledgments
- make sound effects optional
- wheel: don't move weapon description if not in multiwheel mode
- disable Gearbox in intermission screen
- remove partial LZDoom compatibility
v0.3
- fix VM abort when no weapons, AGAIN
- smoother text background image
- make lower limit for text box width
- add optional locked position mode
- more distinct elements color for black or white blocks view
- make displaying weapon tags on weapon change respect GZDoom option
- fix drawing text box on top of wheel when the screen is scaled
- add an option to display weapon tags
- unify fonts, remove font selection
- add weapon tags to blocks view
- add weapon tag display after changing weapons
- remove one parameter from text box drawing function
- move drawing text into a separate file
- move some display-related files into a directory
- move texture cache out of wheel view, cache blocks view textures
- fix text box color and increase background image resolution
- add weapon tag display to wheel
- fix blocks view index
- add open/close sound
- add tick sound
- allow catching more than 2 key bindings
- add a note regarding PyWeaponWheel
- cache view model
- store ammo pip size in cache
- cache scale factor in wheel
- add texture cache to wheel. performance gain is non-existent
- fix warnings, combine coordinate operations into vector operations
- implement ammo display on wheel
- make disabling Gearbox on automap optional
- fix overriding icons from invalid ones from service
- LZDoom half-compatibility: only Blocks view
- add
HideServiceto showDualExplodiumGunfrom SWWM GZ only when appropriate - implement icon patches via Service
- more consistent selection in multiwheel
- multiwheel: click on slot selects the first weapon in this slot
- add patches with icon support
- fix bug with not selecting slot on slot key
- add an option to open menu on slot key
- move slot input check to a function
- don't open gearbox in automap
- fix file conflict with other mods
- add screen dim color option
- fix VM abort on no weapons again
- tweak wheel hands graphics
- remove unnecessary hiding of wheel elements on alpha != 1
- fix weapon selection snapping to selected weapon on fade out
- close weapon selection by altfire
- add smooth wheel graphics by Accensus
v0.2.2
- add gradient to wheel background
- add multiwheel engagement weapon number slider
- prevent opening gearbox while dead
- more reliable freezing while menu is opened
- fix VM abort in hubs
v0.2.1
- add time freezing
- fix multiplayer synchronization
- fix reaction to slot keys in some circumstances
- fix selecting a weapon if it's only one in slot
- add protection against the case when player has no weapons at all
- make colored tint on wheel weapons optional
- add an option to select font for wheel slots
- add little notches on sides of wheel selection bars
v0.2
- reorganize option menu
- add an option for wheel horizontal position
- fix preventing cheat code input in some circumstances
- fix possible VM abort when dropping weapons
- fix scaling for blocks view
- fix scaling wheel pointer
- fix wheel weapon selection scaling
- fix slot label scaling
- remove hardcoded radius
- react to scaling changes
- fix VM abort due to computational error
- fix VM abort with QCDE
- restore indexing the wheel by keys
- multiwheel: indexer works fine
- prettier multiwheel
- WIP multiwheel more
- optimize graphics
- WIP multiwheel
- don't let wheel pointer get from screen bounds
- fix VM abort with blocks view when there is only one weapon
- reset wheel pointer on number keys
- fix VM abort with QCDE
- don't reset wheel state on unrelated input keys
- fix bug with cloning the wheel twice
- add changelog script
- add an option to immediately select weapon if it's only one in slot
- add reaction to number keys
- move screen dimming out of the wheel view and make it optional
- add an option to select the weapon on key release
- implement mouse sensitivity sliders
- don't show ammo2 view if it's the same as ammo1
- add an option to disable mouse input in weapon wheel
- bump required GZDoom version
v0.1: initial release
4. Option menus
4.1. Main
[enu default] GB_OPTIONS = "Gearbox \ch⚙";
AddOptionMenu OptionsMenu { Submenu "$GB_OPTIONS", gb_Options } AddOptionMenu OptionsMenuSimple { Submenu "$GB_OPTIONS", gb_Options } OptionMenu gb_Options { Title "Gearbox" StaticText "" Option "$GB_WEAPONS", gb_view_type, gb_ViewTypeValues Option "$GB_INVENTORY", gb_inventory_view_type, gb_ViewTypeValues Option "$GB_ACTIONS", gb_actions_view_type, gb_ViewTypeValues StaticText "" Submenu "$GB_KEYSECTION", gb_KeysMenu StaticText "" Submenu "$GB_BEHAVIOR_OPTIONS", gb_BehaviorOptions Submenu "$GB_UI_OPTIONS", gb_UiOptions Submenu "$GB_WHEEL_OPTIONS", gb_WheelOptions Submenu "$GB_BLOCKS_OPTIONS", gb_BlocksOptions Submenu "$GB_TEXT_OPTIONS", gb_TextOptions Submenu "$GB_ADVANCED_OPTIONS", gb_AdvancedOptions StaticText "" SafeCommand "$GB_RESET", gb_reset SafeCommand "$GB_RESET_CUSTOM_ORDER", gb_reset_custom_order StaticText "$GB_RESET_CUSTOM_ORDER_NOTE", black } OptionValue gb_ViewTypeValues { 0, "$GB_BLOCKY_VIEW" 1, "$GB_WHEEL_VIEW" 2, "$GB_TEXT_VIEW" }
4.2. Keys
OptionMenu gb_KeysMenu { Title "$GB_KEYSECTION" Control "$GB_TOGGLE_WEAPON_MENU", "+gb_toggle_weapon_menu" Control "$GB_TOGGLE_INVENTORY_MENU", "+gb_toggle_inventory_menu" Control "$GB_TOGGLE_ACTION_MENU", "+gb_toggle_action_menu" StaticText "" Control "$GB_PREV_WEAPON", "gb_prev_weapon" StaticText "" Control "$GB_ROTATE_WEAPON_PRIORITY", "gb_rotate_weapon_priority" Control "$GB_ROTATE_WEAPON_SLOT", "gb_rotate_weapon_slot" }
4.3. Behavior
OptionMenu gb_BehaviorOptions { Title "$GB_BEHAVIOR_OPTIONS_TITLE" Option "$GB_FREEZE_TIME", gb_time_freeze, gb_FreezeValues Option "$GB_ENABLE_ON_AUTOMAP", gb_on_automap, OnOff Option "$GB_FROZEN_CAN_OPEN", gb_frozen_can_open, OnOff StaticText "" StaticText "$CNTRLMNU_WEAPONS" Option "$GB_OPEN_ON_PREV_NEXT", gb_open_on_scroll, gb_OpenViewTypeValues Option "$GB_OPEN_ON_SLOT", gb_open_on_slot, gb_OpenViewTypeValues StaticText "" Option "$GB_REVERSE_SLOT_CYCLE_ORDER", gb_reverse_slot_cycle_order, OffOn Option "$GB_SELECT_FIRST_SLOT_WEAPON", gb_select_first_slot_weapon, OnOff Option "$GB_NO_MENU_IF_ONE", gb_no_menu_if_one, OnOff Option "$GB_SELECT_ON_KEY_UP", gb_select_on_key_up, OnOff Option "$GB_LOCK_POSITION", gb_lock_positions, OnOff StaticText "" StaticText "$CNTRLMNU_INVENTORY" Option "$GB_OPEN_ON_PREV_NEXT", gb_open_inventory_on_scroll, gb_OpenViewTypeValues } OptionValue gb_FreezeValues { 0, "$OPTVAL_OFF" 1, "$GB_LEVEL_AND_PLAYER" 2, "$GB_PLAYER" } OptionValue gb_OpenViewTypeValues { 0, "$OPTVAL_OFF" 1, "$OPTVAL_DEFAULT" 2, "$GB_BLOCKY_VIEW" 3, "$GB_WHEEL_VIEW" 4, "$GB_TEXT_VIEW" }
4.4. Common
OptionMenu gb_UiOptions { Title "$GB_UI_OPTIONS_TITLE" StaticText "" ColorPicker "$GB_COLOR", gb_color Command "$GB_PLAYER_COLOR", gb_copy_player_color StaticText "" Option "$GB_SHOW_DESCRIPTION", gb_show_tags, gb_Tags StaticText "" Option "$GB_PRINT_SELECTION", gb_print_selection, OnOff StaticText "" Option "$GB_DIM", gb_enable_dim, OnOff ColorPicker "$GB_DIM_COLOR", gb_dim_color Option "$GB_BLUR", gb_enable_blur, OnOff StaticText "" Option "$GB_SOUND", gb_enable_sounds, OnOff Option "$GB_SOUNDPACK", gb_soundpack, gb_SoundpackValues, "gb_enable_sounds" StaticText "" Option "$GB_FONT", gb_font, gb_FontValues TextField "$GB_FONT_CUSTOM", gb_font } OptionValue gb_Tags { 0, "$OPTVAL_OFF" 1, "$GB_TAGS" 2, "$GB_CLASSES" } OptionValue gb_SoundpackValues { 0, "Gearbox" 1, "$GB_PACK1" } OptionString gb_FontValues { "NewSmallFont", "$GB_SMALL_FONT" "ConsoleFont", "$GB_CONSOLE_FONT" "BigFont", "$GB_BIG_FONT" }
4.5. Wheel
OptionMenu gb_WheelOptions { Title "$GB_WHEEL_OPTIONS_TITLE" StaticText "" Slider "$GB_WHEEL_X", gb_wheel_position, -1.5, 1.5, 0.1, 1 Slider "$GB_WHEEL_SCALE", gb_wheel_scale, 0.1, 2.0, 0.1, 1 Option "$GB_WHEEL_TINT", gb_wheel_tint, OnOff StaticText "" Option "$GB_MOUSE_IN_WHEEL", gb_mouse_in_wheel, OnOff Slider "$GB_MULTIWHEEL_LIMIT", gb_multiwheel_limit, 3, 100, 1, 0 StaticText "" StaticText "$GB_MOUSE_SENSITIVITY", 1 Slider "$GB_X", gb_mouse_sensitivity_x, 0.1, 5, 0.1, 1 Slider "$GB_Y", gb_mouse_sensitivity_y, 0.1, 5, 0.1, 1 }
4.6. Blocks
OptionMenu gb_BlocksOptions { Title "$GB_BLOCKS_OPTIONS_TITLE" StaticText "" Slider "$GB_SCALE", gb_scale, 1, 8, 1, 0 StaticText "" StaticText "$GB_POSITION", 1 Slider "$GB_X", gb_blocks_position_x, 0.0, 1.0, 0.01, 2 Slider "$GB_Y", gb_blocks_position_y, 0.0, 1.0, 0.01, 2 }
4.7. Plain text
OptionMenu gb_TextOptions { Title "$GB_TEXT_OPTIONS_TITLE" StaticText "" Slider "$GB_SCALE", gb_text_scale, 1, 8, 1, 0 StaticText "" StaticText "$GB_POSITION", 1 Slider "$GB_X", gb_text_position_x, 0.0, 1.0, 0.01, 2 Slider "$GB_Y", gb_text_position_y, 0.0, 1.0, 0.01, 2 Slider "$GB_Y_BOUNDARY", gb_text_position_y_max, 0.0, 1.0, 0.01, 2 StaticText "" Option "$GB_TEXT_USUAL_COLOR", gb_text_usual_color, TextColors Option "$GB_TEXT_SELECTED_COLOR", gb_text_selected_color, TextColors }
4.8. Advanced
OptionMenu gb_AdvancedOptions { Title "$GB_ADVANCED_OPTIONS_TITLE" StaticText "" Option "$GB_VM_ABORT_INFO_ENABLED", gb_vm_abort_report_enabled, OnOff StaticText "" Option "$SCALEMNU_HUDASPECT", hud_AspectScale, OnOff StaticText "$GB_ASPECT_SCALE_NOTE", black }
5. Sounds
// Gearbox gearbox/tick sounds/gb_tick.ogg $volume gearbox/tick 0.5 gearbox/open sounds/gb_toggle.ogg $volume gearbox/open 0.2 gearbox/close sounds/gb_toggle.ogg $volume gearbox/close 0.2 gearbox/nope sounds/gb_nope.ogg $volume gearbox/nope 0.5 // Custom Soundpacks gearbox/pack/tick sounds/gb_tick.ogg $volume gearbox/pack/tick 0.5 gearbox/pack/open sounds/gb_toggle.ogg $volume gearbox/pack/open 0.2 gearbox/pack/close sounds/gb_toggle.ogg $volume gearbox/pack/close 0.2 gearbox/pack/nope sounds/gb_nope.ogg $volume gearbox/pack/nope 0.5
6. Keys
6.1. Aliases
Alias +gb_toggle_weapon_menu "event gb_toggle_weapon_menu" Alias -gb_toggle_weapon_menu "event gb_toggle_weapon_menu_up" Alias +gb_toggle_inventory_menu "event gb_toggle_inventory_menu" Alias -gb_toggle_inventory_menu "" Alias +gb_toggle_action_menu "event gb_toggle_action_menu" Alias -gb_toggle_action_menu "" Alias gb_prev_weapon "netevent gb_prev_weapon" Alias gb_rotate_weapon_priority "event gb_rotate_weapon_priority" Alias gb_rotate_weapon_slot "event gb_rotate_weapon_slot" Alias gb_reset_custom_order "netevent gb_reset_custom_order" Alias gb_report "event gd_report" Alias gb_copy_player_color "gb_color $color" // TODO: do this programmatically. Alias gb_reset "ResetCvar gb_scale; ResetCvar gb_color; ResetCvar gb_dim_color; ResetCvar gb_show_tags; ResetCvar gb_view_type; ResetCvar gb_inventory_view_type; ResetCvar gb_actions_view_type; ResetCvar gb_enable_dim; ResetCvar gb_enable_blur; ResetCvar gb_wheel_position; ResetCvar gb_wheel_scale; ResetCvar gb_wheel_tint; ResetCvar gb_multiwheel_limit; ResetCvar gb_blocks_position_x; ResetCvar gb_blocks_position_y; ResetCvar gb_text_scale; ResetCvar gb_text_position_x; ResetCvar gb_text_position_y; ResetCvar gb_text_position_y_max; ResetCvar gb_text_usual_color; ResetCvar gb_text_selected_color; ResetCvar gb_open_on_scroll; ResetCvar gb_open_on_slot; ResetCvar gb_open_inventory_on_scroll; ResetCvar gb_reverse_slot_cycle_order; ResetCvar gb_select_first_slot_weapon; ResetCvar gb_mouse_in_wheel; ResetCvar gb_select_on_key_up; ResetCvar gb_no_menu_if_one; ResetCvar gb_on_automap; ResetCvar gb_lock_positions; ResetCvar gb_enable_sounds; ResetCvar gb_frozen_can_open; ResetCvar gb_time_freeze; ResetCvar gb_mouse_sensitivity_x; ResetCvar gb_mouse_sensitivity_y;"
6.2. Keys
AddKeySection "$GB_KEYSECTION" gb_Keys AddMenuKey "$GB_TOGGLE_WEAPON_MENU" +gb_toggle_weapon_menu AddMenuKey "$GB_TOGGLE_INVENTORY_MENU" +gb_toggle_inventory_menu AddMenuKey "$GB_TOGGLE_ACTION_MENU" +gb_toggle_action_menu AddMenuKey "$GB_PREV_WEAPON" gb_prev_weapon AddMenuKey "$GB_ROTATE_WEAPON_PRIORITY" gb_rotate_weapon_priority AddMenuKey "$GB_ROTATE_WEAPON_SLOT" gb_rotate_weapon_slot
7. Source
7.1. Behavior
7.1.1. Activity
// This class stores top-level Gearbox state. // It can be either Weapons, Inventory, or None. class gb_Activity { static gb_Activity from() { let result = new("gb_Activity"); result.mActivity = gb_Activity.None; return result; } bool isNone() const { return mActivity == gb_Activity.None; } bool isWeapons() const { return mActivity == gb_Activity.Weapons; } bool isInventory() const { return mActivity == gb_Activity.Inventory; } bool isActions() const { return mActivity == gb_Activity.Actions; } int getOpenViewType() const { return mOpenViewType; } void close() { mActivity = gb_Activity.None; } void openInventory(int openViewType) { mActivity = gb_Activity.Inventory; mOpenViewType = openViewType; } void openWeapons(int openViewType) { mActivity = gb_Activity.Weapons; mOpenViewType = openViewType; } void openActions(int openViewType) { mActivity = gb_Activity.Actions; mOpenViewType = openViewType; } enum _ { None, Weapons, Inventory, Actions, } private int mActivity; private int mOpenViewType; }
7.1.2. Changer
class gb_Changer play { static gb_Changer from(gb_Caption caption, gb_Options options, gb_InventoryUser inventoryUser) { let result = new("gb_Changer"); result.mCaption = caption; result.mOptions = options; result.mInventoryUser = inventoryUser; return result; } void selectWeapon(PlayerInfo player, string weapon) { Weapon targetWeapon = Weapon(player.mo.findInventory(weapon)); if (targetWeapon && gb_WeaponWatcher.currentFor(player) != targetWeapon.getClass()) { player.pendingWeapon = targetWeapon; if (mOptions.isShowingWeaponTagsOnChange()) mCaption.setActor(targetWeapon); } } void useItem(PlayerInfo player, string item) { mInventoryUser.addToQueue(player, item); } void dropItem(PlayerInfo player, string itemString) { Inventory item = player.mo.findInventory(itemString); if (item) player.mo.dropInventory(item, 1); } static void setAngles(PlayerInfo player, double pitch, double angle) { if (player.mo == NULL) return; player.cheats |= CF_InterpView; player.mo.pitch = pitch; player.mo.angle = angle; // To prevent mods that add weapon sway from swaying while moving mouse in wheel. player.cmd.yaw = 0; player.cmd.pitch = 0; } static void freezePlayer(PlayerInfo player, int cheats, vector3 velocity, double gravity) { if (player.mo == NULL) return; player.cheats = cheats; player.vel = velocity.xy; player.mo.vel = velocity; player.mo.gravity = gravity; } private gb_Caption mCaption; private gb_Options mOptions; private gb_InventoryUser mInventoryUser; }
7.1.3. Custom weapon order storage
class gb_WeaponOrderOperations { static gb_WeaponOrderOperations from() { let result = new("gb_WeaponOrderOperations"); result.index.clear(); result.operationType.clear(); return result; } Array<int> index; Array<int> operationType; } class gb_CustomWeaponOrderStorage { enum OperationType { RotatePriority, RotateSlot, } static string calculateHash(gb_WeaponData data) { string dataString; int nWeapons = data.weapons.size(); for (int i = 0; i < nWeapons; ++i) { dataString.appendFormat("%s%d", data.weapons[i].getClassName(), data.slots[i]); } return gb_MD5.hash(dataString); } static void applyOperations(string weaponSetHash, gb_WeaponMenu menu) { let operations = gb_WeaponOrderOperations.from(); string operationsData = loadFromStorage(weaponSetHash); deserializeOperations(operationsData, operations); int nOperations = operations.index.size(); for (int i = 0; i < nOperations; ++i) { switch (operations.operationType[i]) { case RotatePriority: menu.rotatePriorityForIndex(operations.index[i]); break; case RotateSlot: menu.rotateSlotForIndex(operations.index[i]); break; default: Console.printf("Unknown operation: %d.", operations.operationType[i]); } } } static void savePriorityRotation(string weaponSetHash, int index) { saveOperation(weaponSetHash, index, RotatePriority); } static void saveSlotRotation(string weaponSetHash, int index) { saveOperation(weaponSetHash, index, RotateSlot); } static void reset(string weaponSetHash) { Dictionary weaponSetOrders = readWeaponSetOrders(); weaponSetOrders.remove(weaponSetHash); getStorageCvar().setString(weaponSetOrders.toString()); } static void saveOperation(string weaponSetHash, int index, int operationType) { let operations = gb_WeaponOrderOperations.from(); { string operationsData = loadFromStorage(weaponSetHash); deserializeOperations(operationsData, operations); } operations.index .push(index); operations.operationType.push(operationType); string operationsData = serializeOperations(operations); saveToStorage(weaponSetHash, operationsData); } private static void saveToStorage(string hash, string data) { Dictionary weaponSetOrders = readWeaponSetOrders(); weaponSetOrders.insert(hash, data); getStorageCvar().setString(weaponSetOrders.toString()); } private static string loadFromStorage(string hash) { return readWeaponSetOrders().at(hash); } private static Dictionary readWeaponSetOrders() { return Dictionary.fromString(getStorageCvar().getString()); } private static Cvar getStorageCvar() { return Cvar.getCvar(STORAGE_CVAR_NAME); } private static string serializeOperations(gb_WeaponOrderOperations operations) { string result; int nOperations = operations.index.size(); for (int i = 0; i < nOperations; ++i) { result.appendFormat( "%d" .. DELIMITER .. "%d" .. DELIMITER , operations.index[i] , operations.operationType[i] ); } return result; } private static void deserializeOperations(string data, out gb_WeaponOrderOperations operations) { Array<string> tokens; data.split(tokens, DELIMITER, TOK_SkipEmpty); if (tokens.size() % 2 != 0) { Console.printf("Weapon order data is corrupted!"); return; } int nTokens = tokens.size(); for (int i = 0; i < nTokens; i += 2) { operations.index .push(tokens[i ].toInt()); operations.operationType.push(tokens[i + 1].toInt()); } } const STORAGE_CVAR_NAME = "gb_custom_weapon_order"; const DELIMITER = ";"; }
// Custom weapon order storage nosave string gb_custom_weapon_order = "";
7.1.4. Event processor
class gb_EventProcessor { static int process(ConsoleEvent event, bool isSelectOnKeyUp) { if (event.name == "gb_toggle_weapon_menu") return gb_Input.ToggleWeaponMenu; if (event.name == "gb_toggle_weapon_menu_up" && isSelectOnKeyUp) return gb_Input.ConfirmSelection; if (event.name == "gb_toggle_inventory_menu") return gb_Input.ToggleInventoryMenu; if (event.name == "gb_toggle_action_menu") return gb_Input.ToggleActionMenu; if (event.name == "gb_rotate_weapon_priority") return gb_Input.RotateWeaponPriority; if (event.name == "gb_rotate_weapon_slot" ) return gb_Input.RotateWeaponSlot; return gb_Input.Nothing; } }
7.1.5. Font selector
class gb_FontSelector { static gb_FontSelector from() { let result = new("gb_FontSelector"); result.mFontString = gb_Cvar.from("gb_font"); return result; } Font getFont() { string newFontName = mFontString.getString(); if (mOldFontName.length() > 0 && mOldFontName == newFontName) return mFont; if (newFontName == "NewSmallFont") mFont = gb_Fonts.findSmall(); else if (newFontName == "ConsoleFont") mFont = gb_Fonts.findConsole(); else if (newFontName == "BigFont") mFont = gb_Fonts.findBig(); else mFont = Font.getFont(newFontName); if (mFont == NULL && mOldFontName.length() > 0) Console.printf(StringTable.localize("$GB_BAD_FONT"), newFontName); if (mFont == NULL) { mFont = gb_Fonts.findSmall(); mOldFontName = ""; } else { mOldFontName = newFontName; } return mFont; } private gb_Cvar mFontString; private string mOldFontName; private Font mFont; }
7.1.6. Freezer
class gb_Freezer play { static gb_Freezer from(gb_Options options) { let result = new("gb_Freezer"); result.mWasFrozen = false; result.mOptions = options; result.mWasLevelFrozen = false; return result; } void freeze() { if (mWasFrozen) return; mWasFrozen = true; int freezeMode = mOptions.getTimeFreezeMode(); if (isLevelFreezeEnabled (freezeMode)) freezeLevel(); if (isPlayerFreezeEnabled(freezeMode)) freezePlayer(); } void thaw() { if (!mWasFrozen) return; mWasFrozen = false; if (isLevelThawEnabled()) thawLevel(); thawPlayer(); } private static bool isLevelFreezeEnabled(int freezeMode) { return !multiplayer && (freezeMode == 1); } // Freezing level without player causes weird behavior, like weapon bobbing // while Gearbox is open. So, freeze player when level is frozen too. private static bool isPlayerFreezeEnabled(int freezeMode) { return freezeMode != 0; } // Thaw regardless of freeze mode. private static bool isLevelThawEnabled() { return !multiplayer; } private void freezeLevel() { mWasLevelFrozen = level.isFrozen(); level.setFrozen(true); } private void freezePlayer() { mWasPlayerFrozen = true; PlayerInfo player = players[consolePlayer]; mCheats = player.cheats; mVelocity = player.mo.vel; mGravity = player.mo.gravity; gb_Sender.sendFreezePlayerEvent(player.cheats | FROZEN_CHEATS_FLAGS, (0, 0, 0), 0); } private void thawLevel() const { level.setFrozen(mWasLevelFrozen); } private void thawPlayer() const { if (mWasPlayerFrozen) gb_Sender.sendFreezePlayerEvent(mCheats, mVelocity, mGravity); mWasPlayerFrozen = false; } const FROZEN_CHEATS_FLAGS = CF_TotallyFrozen | CF_Frozen; private bool mWasFrozen; private bool mWasLevelFrozen; private bool mWasPlayerFrozen; private int mCheats; private vector3 mVelocity; // to reset weapon bobbing. private double mGravity; private gb_Options mOptions; }
7.1.7. Input
class gb_Input { enum _ { Nothing, SelectNextWeapon, SelectPrevWeapon, ConfirmSelection, Drop, ToggleWeaponMenu, SelectSlot, Close, ToggleInventoryMenu, InventoryPrevious, InventoryNext, RotateWeaponPriority, RotateWeaponSlot, ResetCustomOrder, ToggleActionMenu, } // Returns input type and slot number (for InputSelectSlot only). static int, int getInputFrom(InputEvent event) { if (event.type != InputEvent.Type_KeyDown) return Nothing, 0; string binding = bindings.getBinding(event.keyScan); if (binding == "weapnext" ) return SelectNextWeapon, 0; if (binding == "weapprev" ) return SelectPrevWeapon, 0; if (binding == "+attack" ) return ConfirmSelection, 0; if (binding == "+altattack") return Close, 0; if (binding == "weapdrop" ) return Drop, 0; if (binding == "invdrop" ) return Drop, 0; if (binding == "invprev" ) return InventoryPrevious, 0; if (binding == "invnext" ) return InventoryNext, 0; if (binding.left(5) == "slot ") return SelectSlot, binding.mid(5).toInt(); return Nothing, 0; } }
7.1.8. Inventory menu
class gb_InventoryMenu { static gb_InventoryMenu from(gb_Options options) { let result = new("gb_InventoryMenu"); result.mOptions = options; return result; } string confirmSelection() const { let selected = players[consolePlayer].mo.invSel; if (selected == NULL) return ""; return selected.getClassName(); } ui bool selectNext() { int nItems = getItemsNumber(); if (nItems == 0) return false; int selectedIndex = getSelectedIndex(); setSelectedIndexImpl((selectedIndex + 1) % nItems); return true; } ui bool selectPrev() { int nItems = getItemsNumber(); if (nItems == 0) return false; int selectedIndex = getSelectedIndex(); setSelectedIndexImpl((selectedIndex - 1 + nItems) % nItems); return true; } ui bool setSelectedIndex(int index) { if (index == -1) return false; setSelectedIndexImpl(index); return true; } ui int getSelectedIndex() const { PlayerPawn pawn = players[consolePlayer].mo; int index = 0; for (let item = pawn.inv; item != NULL; item = item.inv) { if (!item.bInvBar) continue; if (item == pawn.invSel) return index; ++index; } return 0; } private ui void setSelectedIndexImpl(int index) { int i = 0; PlayerPawn pawn = players[consolePlayer].mo; for (let item = pawn.inv; item != NULL; item = item.inv) { if (!item.bInvBar) continue; if (i == index) { pawn.invSel = item; break; } ++i; } if (mOptions.isPrintSelection() && 0 <= index && index < getItemsNumber()) Console.printf("%s", pawn.invSel.getTag()); } ui void fill(out gb_ViewModel viewModel) { int index = 0; for (let item = players[consolePlayer].mo.inv; item != NULL; item = item.inv) { if (!item.bInvBar) continue; string tag = item.getTag(); TextureID icon = BaseStatusBar.getInventoryIcon(item, BaseStatusBar.DI_AltIconFirst); viewModel.tags .push(tag); viewModel.classes .push(item.getClassName()); viewModel.slots .push(index + 1); viewModel.indices .push(index); viewModel.icons .push(icon); viewModel.iconScaleXs .push(1); viewModel.iconScaleYs .push(1); viewModel.quantity1 .push(item.maxAmount > 1 ? item.amount : -1); viewModel.maxQuantity1.push(item.maxAmount); viewModel.quantity2 .push(-1); viewModel.maxQuantity2.push(-1); if (icon.isValid()) { viewModel.iconSubstitutes.push(""); viewModel.iconSubstituteColors.push(0); } else { let [substitute, substituteColor] = gb_ViewModel.makeIconSubstitute(item.getClassName()); viewModel.iconSubstitutes.push(substitute); viewModel.iconSubstituteColors.push(substituteColor); } ++index; } viewModel.selectedIndex = getSelectedIndex(); } static int getItemsNumber() { int result = 0; for (let item = players[consolePlayer].mo.inv; item != NULL; item = item.inv) result += item.bInvBar; return result; } private gb_Options mOptions; }
7.1.9. Inventory user
// We could use inventory items directly, but player cannot use items when // totally frozen. Therefore, we have to wait until player isn't frozen. class gb_InventoryUser play { static gb_InventoryUser from() { return new("gb_InventoryUser"); } void use() { for (int i = 0; i < mItemQueue.size();) { PlayerPawn player = mPlayerQueue[i].mo; if (player == NULL) { deleteFromQueue(i); continue; } Inventory item = player.findInventory(mItemQueue[i]); if (item == NULL) { deleteFromQueue(i); continue; } if (player.player.isTotallyFrozen()) { ++i; continue; } else { player.useInventory(item); deleteFromQueue(i); } } } void addToQueue(PlayerInfo player, string item) { mPlayerQueue.push(player); mItemQueue.push(item); } private void deleteFromQueue(int index) { mPlayerQueue.delete(index); mItemQueue.delete(index); } private Array<PlayerInfo> mPlayerQueue; private Array<string> mItemQueue; }
7.1.10. Netevent processor
class gb_NeteventProcessor play { static gb_NeteventProcessor from(gb_Changer changer) { let result = new("gb_NeteventProcessor"); result.mChanger = changer; return result; } int process(ConsoleEvent event) { if (!gb_su.beginsWith(event.name, "gb_")) return gb_Input.Nothing; Array<string> args; event.name.split(args, ":"); PlayerInfo player = players[event.player]; if (args[0] == "gb_select_weapon") mChanger.selectWeapon(player, args[1]); else if (args[0] == "gb_use_item" ) mChanger.useItem (player, args[1]); else if (args[0] == "gb_drop_item" ) mChanger.dropItem (player, args[1]); else if (args[0] == "gb_set_angles" ) mChanger.setAngles(player, args[1].toDouble(), args[2].toDouble()); else if (args[0] == "gb_freeze_player") { mChanger.freezePlayer(player, args[1].toInt(), (args[2].toDouble(), args[3].toDouble(), args[4].toDouble()), args[5].toDouble()); } else if (args[0] == "gb_reset_custom_order") return gb_Input.ResetCustomOrder; return gb_Input.Nothing; } private gb_Changer mChanger; }
7.1.11. Options
// When adding new options, don't forget to add them to: // - keyconf.txt:Alias gb_reset. user int gb_scale = 1; user color gb_color = "22 22 CC"; user color gb_dim_color = "99 99 99"; user int gb_show_tags = 1; user bool gb_print_selection = false; user int gb_view_type = 1; user int gb_inventory_view_type = 1; user int gb_actions_view_type = 1; user bool gb_enable_dim = true; user bool gb_enable_blur = false; user float gb_wheel_position = 1.0; user float gb_wheel_scale = 1.0; user bool gb_wheel_tint = true; user int gb_multiwheel_limit = 12; user float gb_blocks_position_x = 0.0; user float gb_blocks_position_y = 0.0; user int gb_text_scale = 1; user float gb_text_position_x = 0.0; user float gb_text_position_y = 0.0; user float gb_text_position_y_max = 1.0; user int gb_text_usual_color = 21; // cyan user int gb_text_selected_color = 9; // white user string gb_font = "NewSmallFont"; user int gb_open_on_scroll = 0; user int gb_open_on_slot = 1; user int gb_open_inventory_on_scroll = 0; user bool gb_reverse_slot_cycle_order = true; user bool gb_select_first_slot_weapon = false; user int gb_soundpack = 0; user bool gb_mouse_in_wheel = true; user bool gb_select_on_key_up = false; user bool gb_no_menu_if_one = false; user bool gb_on_automap = false; user bool gb_lock_positions = false; user bool gb_enable_sounds = true; user bool gb_frozen_can_open = false; user int gb_time_freeze = 0; user float gb_mouse_sensitivity_x = 1.0; user float gb_mouse_sensitivity_y = 1.0;
class gb_Options { static gb_Options from() { let result = new("gb_Options"); result.mScale = gb_Cvar.from("gb_scale"); result.mColor = gb_Cvar.from("gb_color"); result.mDimColor = gb_Cvar.from("gb_dim_color"); result.mWeaponsViewType = gb_Cvar.from("gb_view_type"); result.mInventoryViewType = gb_Cvar.from("gb_inventory_view_type"); result.mActionsViewType = gb_Cvar.from("gb_actions_view_type"); result.mIsDimEnabled = gb_Cvar.from("gb_enable_dim"); result.mIsBlurEnabled = gb_Cvar.from("gb_enable_blur"); result.mWheelTint = gb_Cvar.from("gb_wheel_tint"); result.mMultiWheelLimit = gb_Cvar.from("gb_multiwheel_limit"); result.mShowTags = gb_Cvar.from("gb_show_tags"); result.mPrintSelection = gb_Cvar.from("gb_print_selection"); result.mShowWeaponTagsOnChange = gb_Cvar.from("DisplayNameTags"); result.mIsPositionLocked = gb_Cvar.from("gb_lock_positions"); result.mFrozenCanOpen = gb_Cvar.from("gb_frozen_can_open"); result.mPreserveAspectRatio = gb_Cvar.from("hud_AspectScale"); result.mOpenOnScroll = gb_Cvar.from("gb_open_on_scroll"); result.mOpenOnSlot = gb_Cvar.from("gb_open_on_slot"); result.mOpenInventoryOnScroll = gb_Cvar.from("gb_open_inventory_on_scroll"); result.mReverseSlotCycleOrder = gb_Cvar.from("gb_reverse_slot_cycle_order"); result.mSelectFirstSlotWeapon = gb_Cvar.from("gb_select_first_slot_weapon"); result.mMouseInWheel = gb_Cvar.from("gb_mouse_in_wheel"); result.mSelectOnKeyUp = gb_Cvar.from("gb_select_on_key_up"); result.mNoMenuIfOne = gb_Cvar.from("gb_no_menu_if_one"); result.mTimeFreeze = gb_Cvar.from("gb_time_freeze"); result.mOnAutomap = gb_Cvar.from("gb_on_automap"); result.mEnableSounds = gb_Cvar.from("gb_enable_sounds"); result.mMouseSensitivityX = gb_Cvar.from("gb_mouse_sensitivity_x"); result.mMouseSensitivityY = gb_Cvar.from("gb_mouse_sensitivity_y"); result.mInvertMouseX = gb_Cvar.from("invertMouseX"); result.mInvertMouseY = gb_Cvar.from("invertMouse"); result.mBlocksPositionX = gb_Cvar.from("gb_blocks_position_x"); result.mBlocksPositionY = gb_Cvar.from("gb_blocks_position_y"); result.mTextScale = gb_Cvar.from("gb_text_scale"); result.mTextPositionX = gb_Cvar.from("gb_text_position_x"); result.mTextPositionY = gb_Cvar.from("gb_text_position_y"); result.mTextPositionYMax = gb_Cvar.from("gb_text_position_y_max"); result.mTextUsualColor = gb_Cvar.from("gb_text_usual_color"); result.mTextSelectedColor = gb_Cvar.from("gb_text_selected_color"); result.mWheelPosition = gb_Cvar.from("gb_wheel_position"); result.mWheelScale = gb_Cvar.from("gb_wheel_scale"); result.mUseSoundpack = gb_Cvar.from("gb_soundpack"); return result; } int getWeaponsViewType() const { return mWeaponsViewType .getInt(); } int getInventoryViewType() const { return mInventoryViewType.getInt(); } int getActionsViewType() const { return mActionsViewType .getInt(); } int getScale() const { return mScale .getInt(); } int getColor() const { return mColor .getInt(); } int getDimColor() const { return mDimColor .getInt(); } bool isDimEnabled() const { return mIsDimEnabled .getBool(); } bool isBlurEnabled() const { return mIsBlurEnabled .getBool(); } bool getWheelTint() const { return mWheelTint .getBool(); } int getMultiWheelLimit() const { return mMultiWheelLimit .getInt(); } int getShowingTags() const { return mShowTags .getInt(); } bool isPrintSelection() const { return mPrintSelection .getBool(); } bool isPositionLocked() const { return mIsPositionLocked .getBool(); } bool isFrozenCanOpen() const { return mFrozenCanOpen .getBool(); } bool isPreservingAspectRatio() const { return mPreserveAspectRatio .getBool(); } int getOpenOnScroll() const { return mOpenOnScroll .getInt(); } int getOpenOnSlot() const { return mOpenOnSlot .getInt(); } int getInventoryOpenOnScroll() const { return mOpenInventoryOnScroll.getInt(); } bool isSlotCycleOrderReversed() const { return mReverseSlotCycleOrder.getBool(); } bool isSelectFirstSlotWeapon() const { return mSelectFirstSlotWeapon.getBool(); } bool isMouseInWheel() const { return mMouseInWheel .getBool(); } bool isSelectOnKeyUp() const { return mSelectOnKeyUp .getBool(); } bool isNoMenuIfOne() const { return mNoMenuIfOne .getBool(); } bool isOnAutomap() const { return mOnAutomap .getBool(); } bool isSoundEnabled() const { return mEnableSounds .getBool(); } int getTimeFreezeMode() const { return mTimeFreeze .getInt(); } int getSoundpack() const { return mUseSoundpack .getInt(); } bool isShowingWeaponTagsOnChange() const { return mShowWeaponTagsOnChange.getInt() & 2; } vector2 getMouseSensitivity() const { double xDirection = mInvertMouseX.getBool() ? -1 : 1; double yDirection = mInvertMouseY.getBool() ? -1 : 1; return (mMouseSensitivityX.getDouble() * xDirection, mMouseSensitivityY.getDouble() * yDirection); } vector2 getBlocksPosition() const { return (mBlocksPositionX.getDouble(), mBlocksPositionY.getDouble()); } int getTextScale() const { return mTextScale .getInt(); } int getTextUsualColor() const { return mTextUsualColor .getInt(); } int getTextSelectedColor() const { return mTextSelectedColor.getInt(); } double getTextPositionYMax() const { return mTextPositionYMax.getDouble(); } vector2 getTextPosition() const { return (mTextPositionX.getDouble(), mTextPositionY.getDouble()); } double getWheelPosition() const { return mWheelPosition.getDouble(); } double getWheelScale() const { return mWheelScale.getDouble(); } private gb_Cvar mScale; private gb_Cvar mColor; private gb_Cvar mDimColor; private gb_Cvar mWeaponsViewType; private gb_Cvar mInventoryViewType; private gb_Cvar mActionsViewType; private gb_Cvar mIsDimEnabled; private gb_Cvar mIsBlurEnabled; private gb_Cvar mWheelTint; private gb_Cvar mMultiWheelLimit; private gb_Cvar mShowTags; private gb_Cvar mPrintSelection; private gb_Cvar mShowWeaponTagsOnChange; private gb_Cvar mIsPositionLocked; private gb_Cvar mOpenOnScroll; private gb_Cvar mOpenOnSlot; private gb_Cvar mOpenInventoryOnScroll; private gb_Cvar mReverseSlotCycleOrder; private gb_Cvar mSelectFirstSlotWeapon; private gb_Cvar mMouseInWheel; private gb_Cvar mSelectOnKeyUp; private gb_Cvar mNoMenuIfOne; private gb_Cvar mTimeFreeze; private gb_Cvar mOnAutomap; private gb_Cvar mEnableSounds; private gb_Cvar mFrozenCanOpen; private gb_Cvar mPreserveAspectRatio; private gb_Cvar mMouseSensitivityX; private gb_Cvar mMouseSensitivityY; private gb_Cvar mInvertMouseX; private gb_Cvar mInvertMouseY; private gb_Cvar mBlocksPositionX; private gb_Cvar mBlocksPositionY; private gb_Cvar mTextScale; private gb_Cvar mTextPositionX; private gb_Cvar mTextPositionY; private gb_Cvar mTextPositionYMax; private gb_Cvar mTextUsualColor; private gb_Cvar mTextSelectedColor; private gb_Cvar mWheelPosition; private gb_Cvar mWheelScale; private gb_Cvar mUseSoundpack; }
7.1.12. Sender
class gb_Sender { static void sendSelectEvent(string className) { EventHandler.sendNetworkEvent(string.format("gb_select_weapon:%s", className)); } static void sendUseItemEvent(string className) { EventHandler.sendNetworkEvent(string.format("gb_use_item:%s", className)); } static void sendDropItemEvent(string className) { EventHandler.sendNetworkEvent(string.format("gb_drop_item:%s", className)); } static void sendFreezePlayerEvent(int cheats, vector3 velocity, double gravity) { EventHandler.sendNetworkEvent(string.format( "gb_freeze_player:%d:%f:%f:%f:%f" , cheats , velocity.x , velocity.y , velocity.z , gravity )); } static void sendPlayerAngles(double pitch, double yaw) { EventHandler.sendNetworkEvent("gb_set_angles:" .. pitch .. ":" .. yaw); } static void sendEvent(string command) { if (command.left(NETEVENT.length()) == NETEVENT) EventHandler.sendNetworkEvent(command.mid(NETEVENT.length() + 1)); else if (command.left(EVENT.length()) == EVENT) EventHandler.sendInterfaceEvent(consolePlayer, command.mid(EVENT.length() + 1)); } const NETEVENT = "netevent"; const EVENT = "event"; }
7.1.13. Sounds
class gb_Sounds { static gb_Sounds from(gb_Options options) { let result = new("gb_Sounds"); result.mOptions = options; return result; } void playTick() { switch(mOptions.GetSoundpack()) { case(0): playSound("gearbox/tick"); break; case(1): playSound("gearbox/pack/tick"); break; DEFAULT: playSound("gearbox/nope"); break; } } void playOpen() { switch(mOptions.GetSoundpack()) { case(0): playSound("gearbox/open"); break; case(1): playSound("gearbox/pack/open"); break; DEFAULT: playSound("gearbox/nope"); break; } } void playClose() { switch(mOptions.GetSoundpack()) { case(0): playSound("gearbox/close"); break; case(1): playSound("gearbox/pack/close"); break; DEFAULT: playSound("gearbox/nope"); break; } } void playNope() { switch(mOptions.GetSoundpack()) { case(0): playSound("gearbox/nope"); break; case(1): playSound("gearbox/pack/nope"); break; DEFAULT: playSound("gearbox/nope"); break; } } private void playSound(string sound) { if (!mOptions.isSoundEnabled()) return; players[consolePlayer].mo.a_StartSound(sound, CHAN_AUTO, CHANF_UI | CHANF_OVERLAP | CHANF_LOCAL); } private gb_Options mOptions; }
7.1.14. View model
struct gb_ViewModel
- Fields
int selectedIndex; Array<string> tags; Array<string> classes; Array<int> slots; Array<int> indices; Array<TextureID> icons; Array<double> iconScaleXs; Array<double> iconScaleYs; Array<string> iconSubstitutes; Array<int> iconSubstituteColors; Array<int> quantity1; Array<int> maxQuantity1; Array<int> quantity2; Array<int> maxQuantity2;
- Is valid
bool isValid() const { int tagCount = tags.size(); return (tagCount > selectedIndex || (tagCount == 0 && selectedIndex == 0)) && tagCount == classes .size() && tagCount == slots .size() && tagCount == indices .size() && tagCount == icons .size() && tagCount == iconScaleXs .size() && tagCount == iconScaleYs .size() && tagCount == iconSubstitutes.size() && tagCount == iconSubstituteColors.size() && tagCount == quantity1 .size() && tagCount == maxQuantity1.size() && tagCount == quantity2 .size() && tagCount == maxQuantity2.size(); }
- Is ammo valid
static bool isAmmoValid(int ammo, int maxAmmo) { return (ammo != -1 && maxAmmo > 0); }
- Print
string print() const { return new("gb_Description") .addInt("selected index", selectedIndex) .addInt("tags", tags .size()) .addInt("classes", classes .size()) .addInt("slots", slots .size()) .addInt("indices", indices .size()) .addInt("icons", icons .size()) .addInt("icon scale Xs", iconScaleXs .size()) .addInt("icon scale Ys", iconScaleYs .size()) .addInt("icon substitutes", iconSubstitutes.size()) .addInt("icon substitute colors", iconSubstituteColors.size()) .addInt("quantities 1", quantity1 .size()) .addInt("max quantities 1", maxQuantity1.size()) .addInt("quantities 2", quantity2 .size()) .addInt("max quantities 2", maxQuantity2.size()) .compose(); }
- Make icon substitute
static string, Color makeIconSubstitute(string key) { int count = key.length(); int seed = 0; for (int i = 0; i < count; ++i) seed += key.byteAt(i); static const string substitutes[] = { "☉", "⊕", "⊖", "⊙", "⊜", "⊞", "⊟", "⊠", "⊡", "∴", "∵", "∶", "∷", "◇", "◆", "◈" }; static const int colors[] = { Font.CR_BRICK, Font.CR_TAN, Font.CR_GREEN, Font.CR_BROWN, Font.CR_RED, Font.CR_BLUE, Font.CR_ORANGE, Font.CR_WHITE, Font.CR_YELLOW, Font.CR_CREAM, Font.CR_OLIVE, Font.CR_DARKGREEN, Font.CR_PURPLE, Font.CR_CYAN, Font.CR_SAPPHIRE, Font.CR_TEAL }; return substitutes[seed & 0xF], colors[(seed & 0xF0) >> 4]; }
7.1.15. Weapon data
struct gb_WeaponData { Array< class<Weapon> > weapons; Array<int> slots; }
7.1.16. Weapon data loader
class gb_WeaponDataLoader play { // Code is adapted from GZDoom: // src/gzdoom/wadsrc/static/zscript/actors/player/player_cheat.zs::CheatGive. static void load(out gb_WeaponData data, bool isOriginalOrder) { gb_WeaponInfo info; PlayerInfo player = players[consolePlayer]; foreach (aClass : AllActorClasses) { let type = (class<Weapon>)(aClass); if (type == NULL || type == "Weapon") continue; // This check from CheatGive doesn't work here. // Why does it allow giving weapons for Treasure Tech there? // Anyway, adding some replaced weapons to the data list won't harm, they // won't show up unless the player has them. //let replacement = Actor.getReplacement(type); //if (replacement != type && !(replacement is "DehackedPickup")) continue; bool located; int slot; int priority; [located, slot, priority] = player.weapons.LocateWeapon(type); if (!located) continue; readonly<Weapon> def = GetDefaultByType(type); if (!def.CanPickup(player.mo)) continue; if (isOriginalOrder) priority = 99 - priority; info.push(type, slot, priority); } sortWeapons(info); data.weapons.move(info.classes); data.slots.move(info.slots); } private static void sortWeapons(gb_WeaponInfo info) { int nWeapons = info.classes.size(); quickSortWeapons(info, 0, nWeapons - 1); } private static void quickSortWeapons(gb_WeaponInfo info, int lo, int hi) { if (lo < hi) { int p = quickSortWeaponsPartition(info, lo, hi); quickSortWeapons(info, lo, p - 1); quickSortWeapons(info, p + 1, hi ); } } private static int quickSortWeaponsPartition(gb_WeaponInfo info, int lo, int hi) { int pivot = measure(info, hi); int i = lo - 1; for (int j = lo; j <= hi - 1; ++j) { if (measure(info, j) <= pivot) { ++i; info.swap(i, j); } } info.swap(i + 1, hi); return i + 1; } private static int measure(gb_WeaponInfo info, int index) { int slot = info.slots[index]; if (slot == 0) slot = 99; int result = slot * 100 + info.priorities[index]; return result; } } struct gb_WeaponInfo { void push(class<Weapon> aClass, int slot, int priority) { classes .push(aClass); slots .push(slot); priorities.push(priority); } void swap(int i, int j) { { let tmp = classes[i]; classes[i] = classes[j]; classes[j] = tmp; } { int tmp = slots[i]; slots[i] = slots[j]; slots[j] = tmp; } { int tmp = priorities[i]; priorities[i] = priorities[j]; priorities[j] = tmp; } } Array< class<Weapon> > classes; Array<int> slots; Array<int> priorities; }
7.1.17. Weapon menu
class gb_WeaponMenu { static gb_WeaponMenu from(gb_WeaponData weaponData, gb_Options options) { let result = new("gb_WeaponMenu"); result.mWeapons.move(weaponData.weapons); result.mSlots.move(weaponData.slots); result.mSelectedIndex = 0; result.mCacheTime = 0; result.mOptions = options; loadIconServices(result.mIconServices); loadHideServices(result.mHideServices); return result; } int getSelectedIndex() const { return mSelectedIndex; } void setSelectedIndex(int index) { if (index == mSelectedIndex) return; mSelectedIndex = index; if (mOptions.isPrintSelection() && 0 <= index && index < mWeapons.size()) { let aWeapon = players[consolePlayer].mo.findInventory(mWeapons[index]); Console.printf("%s", aWeapon.getTag()); } } bool setSelectedIndexFromView(gb_ViewModel viewModel, int index) { if (index == -1 || mSelectedIndex == viewModel.indices[index]) return false; setSelectedIndex(viewModel.indices[index]); return true; } void setSelectedWeapon(class<Weapon> aClass) { if (aClass == NULL) return; int index = getIndexOf(aClass); if (index != mWeapons.size()) setSelectedIndex(index); } ui bool selectNextWeapon() { setSelectedIndex(findNextWeapon()); return mSelectedIndex != mWeapons.size(); } ui bool selectPrevWeapon() { setSelectedIndex(findPrevWeapon()); return mSelectedIndex != mWeapons.size(); } bool selectSlot(int slot, bool selectFirstWeapon = false) { int nWeapons = mWeapons.size(); int startOffset = selectFirstWeapon ? 0 : (mSelectedIndex + 1); for (int i = 0; i < nWeapons; ++i) { int index = (startOffset + nWeapons + 1 * i) % nWeapons; if (mSlots[index] == slot && isInInventory(index) && !isHidden(mWeapons[index].getClassName())) { setSelectedIndex(index); return true; } } return false; } bool isOneWeaponInSlot(int slot) const { int nWeapons = mWeapons.size(); int nWeaponsInSlot = 0; for (int i = 0; i < nWeapons; ++i) { nWeaponsInSlot += (mSlots[i] == slot && isInInventory(i)); if (nWeaponsInSlot > 1) return false; } return nWeaponsInSlot == 1; } bool isInInventory(int index) const { return NULL != players[consolePlayer].mo.findInventory(mWeapons[index]); } string confirmSelection() const { if (mSelectedIndex >= mWeapons.size()) return ""; return getDefaultByType(mWeapons[mSelectedIndex]).getClassName(); } ui void fill(out gb_ViewModel viewModel) { // every other tic. bool isCacheValid = (level.time <= mCacheTime + 1); if (!isCacheValid) { mCacheTime = level.time; mCachedViewModel.tags .clear(); mCachedViewModel.classes .clear(); mCachedViewModel.slots .clear(); mCachedViewModel.indices .clear(); mCachedViewModel.icons .clear(); mCachedViewModel.iconScaleXs .clear(); mCachedViewModel.iconScaleYs .clear(); mCachedViewModel.iconSubstitutes.clear(); mCachedViewModel.iconSubstituteColors.clear(); mCachedViewModel.quantity1 .clear(); mCachedViewModel.maxQuantity1.clear(); mCachedViewModel.quantity2 .clear(); mCachedViewModel.maxQuantity2.clear(); fillDirect(mCachedViewModel); } copy(mCachedViewModel, viewModel); } void rotatePriority() { setSelectedIndex(rotatePriorityForIndex(mSelectedIndex)); } int rotatePriorityForIndex(int index) { bool isIndexFound; int targetIndex; [isIndexFound, targetIndex] = findIndexOfNextWeaponWithSlot(index, mSlots[index]); if (!isIndexFound) return mSelectedIndex; rotate(index, targetIndex); return targetIndex; } void rotateSlot() { setSelectedIndex(rotateSlotForIndex(mSelectedIndex)); } int rotateSlotForIndex(int oldIndex) { int nWeapons = mWeapons.size(); if (nWeapons < 2) return oldIndex; int oldSlot = mSlots[oldIndex]; int newSlot = getNextSlot(oldSlot); int i = 0; for (int slot = 1;;) { while (i < nWeapons && mSlots[i] == slot) ++i; if (slot == newSlot) { mSlots[oldIndex] = newSlot; move(oldIndex, i); return (oldIndex < i) ? i - 1 : i; } slot = getNextSlot(slot); if (slot == 1) break; } return oldIndex; } bool isThereNoWeapons() const { bool isNothingFound = true; int nWeapons = mWeapons.size(); for (int i = 0; i < nWeapons; ++i) { if (isInInventory(i) && !isHidden(mWeapons[i].getClassName())) return false; } return isNothingFound; } private void rotate(int oldIndex, int newIndex) { if (oldIndex > newIndex) move(oldIndex, newIndex); else move(oldIndex, newIndex + 1); } private void move(int oldIndex, int newIndex) { if (oldIndex == newIndex) return; mWeapons.insert(newIndex, mWeapons[oldIndex]); mSlots .insert(newIndex, mSlots [oldIndex]); if (newIndex < oldIndex) ++oldIndex; mWeapons.delete(oldIndex); mSlots .delete(oldIndex); } private bool, int findIndexOfNextWeaponWithSlot(int weaponIndex, int slot) { int nWeapons = mWeapons.size(); for (int i = 1; i < nWeapons; ++i) { int targetCandidateIndex = (weaponIndex + i) % nWeapons; if (mSlots[targetCandidateIndex] == slot) { return true, targetCandidateIndex; } } return false, 0; } private static int getNextSlot(int slot) { if (1 <= slot && slot <= 8) return slot + 1; switch (slot) { case 9: return 0; case 0: return 10; case 10: return 11; case 11: return 1; } Console.printf("Unexpected slot: %d.", slot); return 1; } private static ui void copy(gb_ViewModel source, out gb_ViewModel destination) { destination.selectedIndex = source.selectedIndex; destination.tags .copy(source.tags); destination.classes .copy(source.classes); destination.slots .copy(source.slots); destination.indices .copy(source.indices); destination.icons .copy(source.icons); destination.iconScaleXs .copy(source.iconScaleXs); destination.iconScaleYs .copy(source.iconScaleYs); destination.quantity1 .copy(source.quantity1); destination.maxQuantity1.copy(source.maxQuantity1); destination.quantity2 .copy(source.quantity2); destination.maxQuantity2.copy(source.maxQuantity2); } private ui void fillDirect(out gb_ViewModel viewModel) { int nWeapons = mWeapons.size(); for (int i = 0; i < nWeapons; ++i) { let aWeapon = Weapon(players[consolePlayer].mo.findInventory(mWeapons[i])); if (aWeapon == NULL) { if (mOptions.isPositionLocked()) { TextureID nulltex; nulltex.setInvalid(); viewModel.tags .push(""); viewModel.classes .push(""); viewModel.slots .push(mSlots[i]); viewModel.indices .push(i); viewModel.icons .push(nulltex); viewModel.iconScaleXs .push(-1); viewModel.iconScaleYs .push(-1); viewModel.iconSubstitutes.push("⊗"); viewModel.iconSubstituteColors.push(Font.CR_GRAY); viewModel.quantity1 .push(-1); viewModel.maxQuantity1.push(-1); viewModel.quantity2 .push(-1); viewModel.maxQuantity2.push(-1); } continue; } if (isHidden(aWeapon.getClassName())) continue; if (mSelectedIndex == i) viewModel.selectedIndex = viewModel.tags.size(); viewModel.tags.push(aWeapon.getTag()); viewModel.classes.push(aWeapon.getClassName()); viewModel.slots.push(mSlots[i]); viewModel.indices.push(i); TextureID icon = getIconFor(aWeapon); viewModel.icons.push(icon); viewModel.iconScaleXs.push(aWeapon.scale.x); viewModel.iconScaleYs.push(aWeapon.scale.y); if (icon.isValid()) { viewModel.iconSubstitutes.push(""); viewModel.iconSubstituteColors.push(0); } else { let [substitute, substituteColor] = gb_ViewModel.makeIconSubstitute(aWeapon.getClassName()); viewModel.iconSubstitutes.push(substitute); viewModel.iconSubstituteColors.push(substituteColor); } bool hasAmmo1 = aWeapon.ammo1; bool hasAmmo2 = aWeapon.ammo2 && aWeapon.ammo2 != aWeapon.ammo1; viewModel. quantity1.push(hasAmmo1 ? aWeapon.ammo1. amount : -1); viewModel.maxQuantity1.push(hasAmmo1 ? aWeapon.ammo1.maxAmount : -1); viewModel. quantity2.push(hasAmmo2 ? aWeapon.ammo2. amount : -1); viewModel.maxQuantity2.push(hasAmmo2 ? aWeapon.ammo2.maxAmount : -1); } viewModel.selectedIndex = min(viewModel.selectedIndex, viewModel.tags.size() - 1); } private play bool isHidden(string className) const { bool result = false; foreach (service : mHideServices) { string hideResponse = service.get(className); if (hideResponse.length() != 0) { // convert to bool from "0" or "1". bool isHidden = hideResponse.byteAt(0) - 48; result = isHidden; } } return result; } private ui TextureID getIconFor(Weapon aWeapon) const { TextureID icon = BaseStatusBar.getInventoryIcon(aWeapon, BaseStatusBar.DI_AltIconFirst); { string className = aWeapon.getClassName(); foreach (service : mIconServices) { string iconResponse = service.uiGet(className); if (iconResponse.length() != 0) { TextureID iconFromService = TexMan.checkForTexture(iconResponse, TexMan.Type_Any); if (iconFromService.isValid()) icon = iconFromService; } } } return icon; } private play State getReadyState(Weapon w) const { return w.getReadyState(); } private ui int findNextWeapon() const { return findWeapon( 1); } private ui int findPrevWeapon() const { return findWeapon(-1); } // direction: search direction from the selected weapon: 1 or -1. // // Returns a weapon in the direction. If there is only one weapon, return // it. If there are no weapons, return weapons number. private ui int findWeapon(int direction) const { int nWeapons = mWeapons.size(); // Note range: [1; nWeapons + 1) instead of [0; nWeapons). // This is because I want the current weapon to be found last. for (int i = 1; i < nWeapons + 1; ++i) { int index = (mSelectedIndex + i * direction + nWeapons) % nWeapons; if (isHidden(mWeapons[index].getClassName())) continue; if (isInInventory(index)) return index; } return nWeapons; } private int getIndexOf(class<Weapon> aClass) const { int nWeapons = mWeapons.size(); for (int i = 0; i < nWeapons; ++i) { if (mWeapons[i] == aClass) return i; } return nWeapons; } private static void loadIconServices(out Array<gb_Service> services) { loadServices("gb_IconService", services); } private static void loadHideServices(out Array<gb_Service> services) { loadServices("gb_HideService", services); } private static void loadServices(string serviceName, out Array<gb_Service> services) { let iterator = gb_ServiceIterator.find(serviceName); gb_Service aService; while (aService = iterator.next()) { services.push(aService); } } private Array< class<Weapon> > mWeapons; private Array< int > mSlots; private int mSelectedIndex; private Array<gb_Service> mIconServices; private Array<gb_Service> mHideServices; private gb_ViewModel mCachedViewModel; private int mCacheTime; private gb_Options mOptions; }
7.1.18. Action menu
class gb_ActionMenu
- Constructor
static gb_ActionMenu from() { let result = new("gb_ActionMenu"); return result; }
- Variables
private ui bool mIsInitialized; private ui int mSelectedIndex; private Array<string> mNames; private Array<string> mCommands; private Array<TextureID> mIcons; private Array<string> mIconSubstitutes; private Array<int> mIconSubstituteColors; private Array<int> mOrder; private Array<double> mOnes; private Array<int> mZeroes;
- Initialize
ui void initializeFromUi() { if (mIsInitialized) return; mIsInitialized = true; Map<string, string> commands; collectCommands(MenuDescriptor.getDescriptor("MainMenu"), commands); Map<string, string> aliases; collectAliases(aliases); Map<string, string> events; foreach (name, command : commands) { if (isEvent(command)) { events.insert(name, command); continue; } let [alias, aliasExists] = aliases.checkValue(command); if (aliasExists && isEvent(alias)) events.insert(name, alias); } foreach (key, value : events) { mNames.push(key); mCommands.push(value); let [substitute, substituteColor] = gb_ViewModel.makeIconSubstitute(value); mIconSubstitutes.push(substitute); mIconSubstituteColors.push(substituteColor); } rememberBlanks(); } private static ui bool isEvent(string command) { return command.left("netevent".length()) ~== "netevent" || command.left("event".length()) ~== "event"; }
- Collect commands
Returns a map with name-command pairs.
private static ui void collectCommands(MenuDescriptor descriptor, out Map<string, string> result) { let optionDescriptor = OptionMenuDescriptor(descriptor); if (optionDescriptor) { foreach (item : optionDescriptor.mItems) { if (item is "OptionMenuItemSubmenu") collectCommands(MenuDescriptor.getDescriptor(item.getAction()), result); else if (item is "OptionMenuItemControlBase") { string command = item.getAction(); result.insert(item.mLabel, command.makeLower()); } } return; } let listDescriptor = ListMenuDescriptor(descriptor); if (listDescriptor) { foreach (item : listDescriptor.mItems) { if (item is "ListMenuItemSelectable") collectCommands(MenuDescriptor.getDescriptor(item.getAction()), result); } } }
- Collect aliases
Returns a map with alias-command pairs.
This functions covers only the most basic aliases. Good enough for now.
private ui void collectAliases(out Map<string, string> result) { for (int i = Wads.findLump(KEYCONF, 0); i != -1; i = Wads.findLump(KEYCONF, i + 1)) { Array<string> lines; Wads.readLump(i).split(lines, "\n", TOK_SKIPEMPTY); foreach (line : lines) { Array<string> words; line.split(words, " ", TOK_SKIPEMPTY); if (words.size() < 3 || !(words[0] ~== "alias")) continue; string name = words[1]; words.delete(0, 2); string command = gb_su.join(words, " "); command = command.mid(1, command.length() - 2); result.insert(name, command.makeLower()); } } } const KEYCONF = "keyconf";
- Remember blanks
private ui void rememberBlanks() { int eventCount = mNames.size(); TextureID nulltex; nulltex.setInvalid(); mIcons.resize(eventCount); for (int i = 0; i < eventCount; ++i) mIcons[i] = nulltex; mOrder.resize(eventCount); for (int i = 0; i < eventCount; ++i) mOrder[i] = i; mOnes.resize(eventCount); for (int i = 0; i < eventCount; ++i) mOnes[i] = 1.0; mZeroes.resize(eventCount); for (int i = 0; i < eventCount; ++i) mZeroes[i] = 0; }
- Fill
ui void fill(out gb_ViewModel viewModel) { viewModel.selectedIndex = mSelectedIndex; viewModel.tags.copy(mNames); viewModel.classes.copy(mCommands); viewModel.slots.copy(mOrder); viewModel.indices.copy(mOrder); viewModel.icons.copy(mIcons); viewModel.iconScaleXs.copy(mOnes); viewModel.iconScaleYs.copy(mOnes); viewModel.iconSubstitutes.copy(mIconSubstitutes); viewModel.iconSubstituteColors.copy(mIconSubstituteColors); viewModel.quantity1.copy(mZeroes); viewModel.maxQuantity1.copy(mZeroes); viewModel.quantity2.copy(mZeroes); viewModel.maxQuantity2.copy(mZeroes); }
- Select previous and next
ui bool selectPrev() { int namesCount = mNames.size(); mSelectedIndex = (mSelectedIndex + namesCount - 1) % namesCount; return true; } ui bool selectNext() { mSelectedIndex = (mSelectedIndex + 1) % mNames.size(); return true; }
- Selected index
ui int getSelectedIndex() const { return mSelectedIndex; } ui bool setSelectedIndex(int index) { if (index == -1) return false; mSelectedIndex = index; return true; }
- Confirm selection
ui string confirmSelection() const { return mCommands[mSelectedIndex]; }
7.2. Display
7.2.1. Blocky view
class gb_BlockyView { static gb_BlockyView from(gb_TextureCache textureCache, gb_Options options, gb_FontSelector fontSelector) { let result = new("gb_BlockyView"); result.setAlpha(1.0); result.setScale(1); result.setBaseColor(0x2222CC); result.mTextureCache = textureCache; result.mOptions = options; result.mFontSelector = fontSelector; return result; } void setAlpha(double alpha) { mAlpha = alpha; } void setScale(int scale) { if (scale < 1) scale = 1; double scaleFactor = getScaleFactor(); mScreenWidth = int(Screen.getWidth() / scale / scaleFactor); mScreenHeight = int(Screen.getHeight() / scale / scaleFactor); } void setBaseColor(int color) { mBaseColor = color; mSlotColor = addColor(mBaseColor, -0x22); mSelectedColor = addColor(mBaseColor, 0x44); mAmmoBackColor = addColor(mBaseColor, 0x66); } void display(gb_ViewModel viewModel) const { int selectedIndex = viewModel.selectedIndex; if (selectedIndex >= viewModel.slots.size() || selectedIndex == -1) return; vector2 position = mOptions.getBlocksPosition(); int maxWidth = getMaxWidth(getSlotsNumber(viewModel)); int maxHeight = getMaxHeight(viewModel); int startX = int(min(BORDER + (mScreenWidth - BORDER) * position.x, mScreenWidth - maxWidth)); int startY = int(min(BORDER + (mScreenHeight - BORDER) * position.y, mScreenHeight - maxHeight)); int lastDrawnSlot = 0; int slotX = startX; int inSlotIndex = 0; int selectedSlot = viewModel.slots[selectedIndex]; Font aFont = mFontSelector.getFont(); int fontHeight = aFont.getHeight(); int textY = startY + SLOT_SIZE / 2 - fontHeight / 2; int nWeapons = viewModel.tags.size(); for (int i = 0; i < nWeapons; ++i) { int slot = viewModel.slots[i]; // slot number box if (slot != lastDrawnSlot) { drawAlphaTexture(mTextureCache.blockBox, slotX, startY, mSlotColor); string slotText = string.format("%d", slot); int textX = slotX + SLOT_SIZE / 2 - aFont.stringWidth(slotText) / 2; drawText(aFont, Font.CR_WHITE, textX, textY, slotText); lastDrawnSlot = slot; } if (slot == selectedSlot) // selected slot (big boxes) { bool isSelectedWeapon = (i == selectedIndex); int weaponColor = isSelectedWeapon ? mSelectedColor : mBaseColor; int weaponY = startY + SLOT_SIZE + (SELECTED_WEAPON_HEIGHT + MARGIN) * inSlotIndex; // big box drawAlphaTexture(mTextureCache.blockBig, slotX, weaponY, weaponColor); // weapon { // code is adapted from GZDoom AltHud.DrawImageToBox. TextureID weaponTexture = viewModel.icons[i]; let weaponSize = TexMan.getScaledSize(weaponTexture); weaponSize.x *= viewModel.iconScaleXs[i]; weaponSize.y *= viewModel.iconScaleYs[i]; if (mOptions.isPreservingAspectRatio()) weaponSize.y *= 1.2; int allowedWidth = SELECTED_SLOT_WIDTH - MARGIN * 2; int allowedHeight = SELECTED_WEAPON_HEIGHT - MARGIN * 2; double scaleHor = (allowedWidth < weaponSize.x) ? allowedWidth / weaponSize.x : 1.0; double scaleVer = (allowedHeight < weaponSize.y) ? allowedHeight / weaponSize.y : 1.0; double scale = min(scaleHor, scaleVer); int weaponWidth = int(round(weaponSize.x * scale)); int weaponHeight = int(round(weaponSize.y * scale)); drawWeapon( weaponTexture , slotX + SELECTED_SLOT_WIDTH / 2 , weaponY + SELECTED_WEAPON_HEIGHT / 2 , weaponWidth , weaponHeight ); } // corners if (isSelectedWeapon) { int lineEndX = slotX + SELECTED_SLOT_WIDTH - CORNER_SIZE; int lineEndY = weaponY + SELECTED_WEAPON_HEIGHT - CORNER_SIZE; TextureID cornerTexture = mTextureCache.corner; // top left, top right, bottom left, bottom right drawFlippedTexture(cornerTexture, slotX, weaponY, No, No); drawFlippedTexture(cornerTexture, lineEndX, weaponY, Yes, No); drawFlippedTexture(cornerTexture, slotX, lineEndY, No, Yes); drawFlippedTexture(cornerTexture, lineEndX, lineEndY, Yes, Yes); } // quantity indicators TextureID ammoTexture = mTextureCache.ammoLine; int ammoY = weaponY; if (gb_ViewModel.isAmmoValid(viewModel.quantity1[i], viewModel.maxQuantity1[i])) { drawAlphaTexture( ammoTexture , slotX + MARGIN * 2 , ammoY , mAmmoBackColor ); int ammoRatioWidth = ammoRatioWidthFor(viewModel.quantity1[i], viewModel.maxQuantity1[i]); drawAlphaWidthTexture( ammoTexture , slotX + MARGIN * 2 , ammoY , FILLED_AMMO_COLOR , ammoRatioWidth ); ammoY += MARGIN + AMMO_HEIGHT; } if (gb_ViewModel.isAmmoValid(viewModel.quantity2[i], viewModel.maxQuantity2[i])) { drawAlphaTexture( ammoTexture , slotX + MARGIN * 2 , ammoY , mAmmoBackColor ); int ammoRatioWidth = ammoRatioWidthFor(viewModel.quantity2[i], viewModel.maxQuantity2[i]); drawAlphaWidthTexture( ammoTexture , slotX + MARGIN * 2 , ammoY , FILLED_AMMO_COLOR , ammoRatioWidth ); } int showingTags = mOptions.getShowingTags(); if (showingTags == 1) drawTag(viewModel.tags[i], aFont, slotX, weaponY); if (showingTags == 2) drawTag(viewModel.classes[i], aFont, slotX, weaponY); } else // not selected slot (small boxes) { int boxY = startY - MARGIN + (SLOT_SIZE + MARGIN) * (inSlotIndex + 1); drawAlphaTexture(mTextureCache.blockBox, slotX, boxY, mBaseColor); } if (i + 1 < nWeapons && viewModel.slots[i + 1] != slot) { slotX += ((slot == selectedSlot) ? SELECTED_SLOT_WIDTH : SLOT_SIZE) + MARGIN; inSlotIndex = 0; } else { ++inSlotIndex; } } } private static int getSlotsNumber(gb_ViewModel viewModel) { int nSlots = 0; int lastSlot = -1; foreach (aSlot : viewModel.slots) { if (aSlot != lastSlot) { ++nSlots; lastSlot = aSlot; } } return viewModel.slots.size(); } private static int getMaxWidth(int nSlots) { return (nSlots - 1) * (SLOT_SIZE + MARGIN) + SELECTED_SLOT_WIDTH + BORDER; } private static int getMaxHeight(gb_ViewModel viewModel) { int nEntries = viewModel.slots.size(); int lastSlot = -1; int itemsInSlot = 1; int maxItemsInSlot = -1; for (int i = 0; i < nEntries; ++i) { if (viewModel.slots[i] == lastSlot) { ++itemsInSlot; } else { lastSlot = viewModel.slots[i]; if (itemsInSlot > maxItemsInSlot) maxItemsInSlot = itemsInSlot; itemsInSlot = 1; } } if (itemsInSlot > maxItemsInSlot) maxItemsInSlot = itemsInSlot; return maxItemsInSlot * (SELECTED_WEAPON_HEIGHT + MARGIN) - MARGIN + BORDER + SLOT_SIZE; } private void drawTag(string tag, Font aFont, double startX, double startY) { // >_< // // Trying to put as much text into the the box as possible, while gracefully // (hopefully) handling when the whole tag cannot fit in the box. // // This code doesn't take the icon and quantity bars into account. Just print // semi-transparent text over them. if (tag.length() == 0) return; Array<string> words; tag.split(words, " ", Tok_SkipEmpty); // Start filling lines with words from bottom to top. If the word doesn't // fit into the line, it's pushed into the new top line. Array<string> lines; int nWords = words.size(); lines.push(words[nWords - 1]); int spaceWidth = aFont.stringWidth(" "); int allowedStringWidth = SELECTED_SLOT_WIDTH - 2; for (int i = nWords - 2; i >= 0; --i) { int newWidth = (aFont.stringWidth(lines[0]) + spaceWidth + aFont.stringWidth(words[i])); if (newWidth < allowedStringWidth) lines[0] = words[i] .. " " .. lines[0]; else lines.insert(0, words[i]); } // If there are too many lines, put them on the third line and mark the it // with ellipsis. string ellipsis = aFont.getGlyphHeight(ELLIPSIS_CODE) ? "…" : "..."; int nLines = lines.size(); if (nLines > 3) { for (int i = 3; i < nLines; ++i) lines[2].appendFormat(" %s", lines[i]); lines[2] = lines[2] .. ellipsis; } // If a line is too long to fit in the box, replace the part that doesn't // fit with ellipsis. int linesEnd = min(nLines, 3); int ellipsisWidth = aFont.stringWidth(ellipsis); for (int i = 0; i < linesEnd; ++i) { if (aFont.stringWidth(lines[i]) <= allowedStringWidth) continue; while (aFont.stringWidth(lines[i]) + ellipsisWidth > allowedStringWidth) { lines[i].deleteLastCharacter(); } lines[i] = lines[i] .. ellipsis; } // Finally, print lines. int lineHeight = aFont.getHeight(); for (int i = 0; i < linesEnd; ++i) { double y = startY + SELECTED_WEAPON_HEIGHT + (i - linesEnd) * lineHeight; drawText(aFont, Font.CR_WHITE, startX + 1, y, lines[i], 0.3); } } private static int ammoRatioWidthFor(int ammoCount, int ammoMax) { return int(round(float(ammoCount) / ammoMax * AMMO_WIDTH)); } private void drawAlphaTexture(TextureID texture, int x, int y, int color) const { Screen.drawTexture( texture , NO_ANIMATION , x , y , DTA_FillColor , color , DTA_AlphaChannel , true , DTA_Alpha , mAlpha , DTA_VirtualWidth , mScreenWidth , DTA_VirtualHeight , mScreenHeight , DTA_KeepRatio , true ); } private void drawAlphaWidthTexture(TextureID texture, int x, int y, int color, int width) const { Screen.drawTexture( texture , NO_ANIMATION , x , y , DTA_FillColor , color , DTA_AlphaChannel , true , DTA_Alpha , mAlpha , DTA_VirtualWidth , mScreenWidth , DTA_VirtualHeight , mScreenHeight , DTA_KeepRatio , true , DTA_DestWidth , width ); } enum FlipOptions { No = 0, Yes = 1, } private void drawFlippedTexture(TextureID texture, int x, int y, int horFlip, int verFlip) const { Screen.drawTexture( texture , NO_ANIMATION , x , y , DTA_Alpha , mAlpha , DTA_VirtualWidth , mScreenWidth , DTA_VirtualHeight , mScreenHeight , DTA_KeepRatio , true , DTA_FlipX , horFlip , DTA_FlipY , verFlip ); } private void drawWeapon(TextureID texture, int x, int y, int w, int h) const { Screen.drawTexture( texture , NO_ANIMATION , x , y , DTA_CenterOffset , true , DTA_KeepRatio , true , DTA_DestWidth , w , DTA_DestHeight , h , DTA_Alpha , mAlpha , DTA_VirtualWidth , mScreenWidth , DTA_VirtualHeight , mScreenHeight ); } private void drawText(Font aFont, int color, double x, double y, string text, double alpha = 1.0) const { Screen.drawText( aFont , color , x , y , text , DTA_Alpha , mAlpha * alpha , DTA_VirtualWidth , mScreenWidth , DTA_VirtualHeight , mScreenHeight , DTA_KeepRatio , true ); } private static color addColor(color base, int add) { uint newRed = clamp(base.r + add, 0, 255); uint newGreen = clamp(base.g + add, 0, 255); uint newBlue = clamp(base.b + add, 0, 255); if ( abs(int(newRed ) - base.r) < abs(add) && abs(int(newGreen) - base.g) < abs(add) && abs(int(newBlue ) - base.b) < abs(add) ) { return addColor(base, -add); } uint result = (newRed << 16) + (newGreen << 8) + newBlue; return result; } private static double getScaleFactor() { return Screen.getHeight() / 1080.0; } const BORDER = 20; const MARGIN = 4; const SLOT_SIZE = 25; const SELECTED_SLOT_WIDTH = 100; const SELECTED_WEAPON_HEIGHT = SLOT_SIZE * 2 + MARGIN; const NO_ANIMATION = 0; // == false const CORNER_SIZE = 4; const AMMO_WIDTH = 40; const AMMO_HEIGHT = 6; const FILLED_AMMO_COLOR = 0x22DD22; const ELLIPSIS_CODE = 8230; private double mAlpha; private int mScreenWidth; private int mScreenHeight; private color mBaseColor; private color mSlotColor; private color mSelectedColor; private color mAmmoBackColor; private gb_TextureCache mTextureCache; private gb_Options mOptions; private gb_FontSelector mFontSelector; }
7.2.2. Blur
class gb_Blur { static void setEnabled(bool isEnabled) { Shader.setEnabled(players[consolePlayer], "gb_blur", isEnabled); } }
HardwareShader postprocess scene { Name "gb_blur" Shader "shaders/mfx_bss_blur.fp" 330 }
// SPDX-FileCopyrightText: © 2012-2019 Marisa Kirisame /* BlurSharpShift blur from MariENB (C)2012-2019 Marisa Kirisame https://git.sayachan.org/OrdinaryMagician/marifx_m Modified by m8f 2021 (made radius a constant). */ void main() { vec2 coord = TexCoord; vec4 res = texture(InputTexture,coord); vec2 ofs[16] = vec2[] ( vec2(1.0,1.0), vec2(-1.0,-1.0), vec2(-1.0,1.0), vec2(1.0,-1.0), vec2(1.0,0.0), vec2(-1.0,0.0), vec2(0.0,1.0), vec2(0.0,-1.0), vec2(1.41,0.0), vec2(-1.41,0.0), vec2(0.0,1.41), vec2(0.0,-1.41), vec2(1.41,1.41), vec2(-1.41,-1.41), vec2(-1.41,1.41), vec2(1.41,-1.41) ); vec2 bresl = vec2(textureSize(InputTexture,0)); float radius = 2.0; vec2 bof = radius/bresl; for ( int i=0; i<16; i++ ) res.rgb += texture(InputTexture,coord+ofs[i]*bof).rgb; res.rgb /= 17.0; FragColor = res; }
7.2.3. Caption
class gb_Caption { static gb_Caption from() { let result = new("gb_Caption"); result.mActor = NULL; return result; } void setActor(Actor anActor) { mActor = anActor; } ui void show() { if (mActor == NULL) return; mActor.displayNameTag(); mActor = NULL; } private Actor mActor; }
7.2.4. Dim
class gb_Dim { static void dim(double alpha, gb_Options options) { if (!options.isDimEnabled()) return; Screen.Dim(options.getDimColor(), alpha * 0.4, 0, 0, Screen.getWidth(), Screen.getHeight()); } }
7.2.5. Fade in out
class gb_FadeInOut { static gb_FadeInOut from() { let result = new("gb_FadeInOut"); result.mAlpha = 0.0; return result; } void fadeInOut(double step) { mAlpha = clamp(mAlpha + step, 0.0, 1.0); } double getAlpha() const { return mAlpha; } private double mAlpha; }
7.2.6. Plain text view
class gb_TextView { static gb_TextView from(gb_Options options, gb_FontSelector fontSelector) { let result = new("gb_TextView"); result.setAlpha(1.0); result.setScale(1); result.mOptions = options; result.mFontSelector = fontSelector; return result; } void setAlpha(double alpha) { mAlpha = alpha; } void setScale(int scale) { if (scale < 1) scale = 1; double scaleFactor = getScaleFactor(); mScreenWidth = int(Screen.getWidth() / scale / scaleFactor); mScreenHeight = int(Screen.getHeight() / scale / scaleFactor); } void display(gb_ViewModel viewModel) const { int nWeapons = viewModel.tags.size(); if (nWeapons == 0) return; vector2 start = mOptions.getTextPosition(); start.x *= mScreenWidth; start.y *= mScreenHeight; double x = start.x; double y = start.y; Font aFont = mFontSelector.getFont(); double lineHeight = aFont.getHeight(); int usualColor = mOptions.getTextUsualColor(); int selectedColor = mOptions.getTextSelectedColor(); bool isTagOrClass = mOptions.getShowingTags() != 2; double maxX = 0; double maxY = 0; // dry run int maxSlotWidth = 0; for (int i = 0; i < nWeapons; ++i) { string slotString = string.format("%d. ", viewModel.slots[i]); maxSlotWidth = max(maxSlotWidth, aFont.stringWidth(slotString)); } for (int i = 0; i < nWeapons; ++i) { string itemString = makeItemString(viewModel, i, isTagOrClass); maxX = max(maxX, maxSlotWidth + x + aFont.stringWidth(itemString)); y += lineHeight; if (i + 1 < nWeapons && viewModel.slots[i] != viewModel.slots[i + 1]) y += lineHeight / 3; } maxY = y; if (maxX > mScreenWidth) x = max(0, mScreenWidth - (maxX - x)); double yBoundary = mScreenHeight * mOptions.getTextPositionYMax(); if (maxY > yBoundary && (maxY - lineHeight != start.y)) { lineHeight *= (yBoundary - lineHeight - start.y) / (maxY - lineHeight - start.y); } y = start.y; // real run double selectedY = 0; for (int i = 0; i < nWeapons; ++i) { if (i == viewModel.selectedIndex) { selectedY = y; } else { string slotString = string.format("%d. ", viewModel.slots[i]); drawText(aFont, usualColor, x, y, slotString, mAlpha); } y += lineHeight; if (i + 1 < nWeapons && viewModel.slots[i] != viewModel.slots[i + 1]) y += lineHeight / 3; } { string slotString = string.format("%d. ", viewModel.slots[viewModel.selectedIndex]); drawText(aFont, selectedColor, x, selectedY, slotString, mAlpha); } y = start.y; for (int i = 0; i < nWeapons; ++i) { if (i != viewModel.selectedIndex) { string itemText = makeItemString(viewModel, i, isTagOrClass); drawText(aFont, usualColor, x + maxSlotWidth, y, itemText, mAlpha); } y += lineHeight; if (i + 1 < nWeapons && viewModel.slots[i] != viewModel.slots[i + 1]) y += lineHeight / 3; } { string itemText = makeItemString(viewModel, viewModel.selectedIndex, isTagOrClass); drawText(aFont, selectedColor, x + maxSlotWidth, selectedY, itemText, mAlpha); } } private string makeItemString(gb_ViewModel viewModel, int i, bool isTagOrClass) { string result = isTagOrClass ? viewModel.tags[i] : viewModel.classes[i]; bool isQuantity1Valid = gb_ViewModel.isAmmoValid(viewModel.quantity1[i], viewModel.maxQuantity1[i]); bool isQuantity2Valid = gb_ViewModel.isAmmoValid(viewModel.quantity2[i], viewModel.maxQuantity2[i]); if (isQuantity1Valid && isQuantity2Valid) { result.appendFormat(" (%d/%d, %d/%d)" , viewModel.quantity1[i] , viewModel.maxQuantity1[i] , viewModel.quantity2[i] , viewModel.maxQuantity2[i] ); } else if (isQuantity1Valid) { result.appendFormat(" (%d/%d)", viewModel.quantity1[i], viewModel.maxQuantity1[i]); } else if (isQuantity2Valid) { result.appendFormat(" (%d/%d)", viewModel.quantity2[i], viewModel.maxQuantity2[i]); } return result; } private void drawText(Font aFont, int color, double x, double y, string text, double alpha = 1.0) const { Screen.drawText( aFont , color , x , y , text , DTA_Alpha , mAlpha * alpha , DTA_VirtualWidth , mScreenWidth , DTA_VirtualHeight , mScreenHeight , DTA_KeepRatio , true ); } private static double getScaleFactor() { return Screen.getHeight() / 1080.0; } private double mAlpha; private int mScreenWidth; private int mScreenHeight; private gb_Options mOptions; private gb_FontSelector mFontSelector; }
7.3. Engine
7.3.1. Level
class gb_Level { enum _ { NotInGame, Loading, JustLoaded, Loaded } static int getState() { // In the best case, the world is fully loaded on tick 0, but if player // weapons are configured via keyconf, they require a bit of network // communication, which finishes at tick 1. if (level.mapName ~== "titlemap") return gb_Level.NotInGame; if (level.mapTime == 0) return gb_Level.Loading; if (level.mapTime == 1) return gb_Level.JustLoaded; return gb_Level.Loaded; } }
7.3.2. Weapon watcher
// This class provides information on what weapon the players hold. class gb_WeaponWatcher { static class<Weapon> current() { return currentFor(players[consolePlayer]); } static class<Weapon> currentFor(PlayerInfo player) { if (player.pendingWeapon == NULL) return NULL; let currentWeapon = player.pendingWeapon.getClassName() != "Object" ? player.pendingWeapon : player.readyWeapon; return currentWeapon ? currentWeapon.getClass() : NULL; } }
7.4. Event handler
This class is the core of Gearbox.
It delegates as much work to other classes while minimizing the relationships between those classes.
For multiplayer compatibility:
- All visuals and input processing happens on client side and is invisible to the network.
- Actual game changing things, like switching weapons, are done through network - even for the current player, even for the single-player game.
class gb_EventHandler : EventHandler
7.4.1. World tick
override void worldTick() { switch (gb_Level.getState()) { case gb_Level.NotInGame: return; case gb_Level.Loading: return; case gb_Level.JustLoaded: initialize(); // fall through case gb_Level.Loaded: break; } bool isClosed = mActivity.isNone(); // Thaw regardless of the option to prevent player being locked frozen after // changing options. if (isClosed) mFreezer.thaw(); else mFreezer.freeze(); if (!isClosed && (isDead() || isDisabledOnAutomap())) { close(); } if (isClosed) { // Watch for the current weapon, because player can change it without // Gearbox. Also handles the case when Gearbox hasn't been opened yet, // initializing weapon menu. mWeaponMenu.setSelectedWeapon(gb_WeaponWatcher.current()); } else if (getActiveViewType() == 1) { mWheelController.process(); } mInventoryUser.use(); }
7.4.2. Console process
This function processes key bindings specific for Gearbox.
override void consoleProcess(ConsoleEvent event) { if (players[consolePlayer].mo == NULL || !mIsInitialized || isDisabledOnAutomap() || (isPlayerFrozen() && mActivity.isNone())) return; switch (gb_EventProcessor.process(event, mOptions.isSelectOnKeyUp())) { case gb_Input.ToggleWeaponMenu: if (mActivity.isWeapons()) close(); else openWeapons(1); break; case gb_Input.ToggleInventoryMenu: if (mActivity.isInventory()) close(); else openInventory(1); break; case gb_Input.ToggleActionMenu: if (mActivity.isActions()) close(); else openActions(1); break; case gb_Input.ConfirmSelection: confirmSelection(); close(); break; case gb_Input.RotateWeaponPriority: rotateWeaponPriority(); break; case gb_Input.RotateWeaponSlot: rotateWeaponSlot(); break; } if (!mActivity.isNone()) mWheelController.reset(); }
7.4.3. Input process
This function provides latching to existing key bindings, and processing mouse input.
override bool inputProcess(InputEvent event) { if (players[consolePlayer].mo == NULL || !mIsInitialized || isDisabledOnAutomap() || gameState != GS_LEVEL || (isPlayerFrozen() && mActivity.isNone())) return false; let [input, slot] = gb_Input.getInputFrom(event); if (mActivity.isWeapons()) return processInputInWeapons(input, slot); if (mActivity.isInventory()) return processInputInInventory(input, slot); if (mActivity.isActions()) return processInputInActions(input, slot); if (mActivity.isNone()) return processInputInNone(input, slot); throwAbortException("ERROR: unknown activity state"); return false; }
7.4.4. Process input in weapons
private ui bool processInputInWeapons(int input, int slot) { switch (input) { case gb_Input.SelectNextWeapon: tickIf(mWeaponMenu.selectNextWeapon()); mWheelController.reset(); return true; case gb_Input.SelectPrevWeapon: tickIf(mWeaponMenu.selectPrevWeapon()); mWheelController.reset(); return true; case gb_Input.ConfirmSelection: confirmSelection(); close(); return true; case gb_Input.Drop: dropItem(); return true; case gb_Input.Close: close(); return true; case gb_Input.SelectSlot: mWheelController.reset(); tickIf(mWeaponMenu.selectSlot(slot)); return true; } return false; }
7.4.5. Process input in inventory
private ui bool processInputInInventory(int input, int slot) { switch (input) { case gb_Input.SelectNextWeapon: tickIf(mInventoryMenu.selectNext()); mWheelController.reset(); return true; case gb_Input.SelectPrevWeapon: tickIf(mInventoryMenu.selectPrev()); mWheelController.reset(); return true; case gb_Input.ConfirmSelection: confirmSelection(); close(); return true; case gb_Input.Drop: dropItem(); return true; case gb_Input.Close: close(); return true; case gb_Input.SelectSlot: mWheelController.reset(); tickIf(mInventoryMenu.setSelectedIndex((slot == 0) ? 9 : slot - 1)); return true; } return false; }
7.4.6. Process input in actions
private ui bool processInputInActions(int input, int slot) { switch (input) { case gb_Input.SelectNextWeapon: tickIf(mActionMenu.selectNext()); mWheelController.reset(); return true; case gb_Input.SelectPrevWeapon: tickIf(mActionMenu.selectPrev()); mWheelController.reset(); return true; case gb_Input.ConfirmSelection: confirmSelection(); close(); return true; case gb_Input.Close: close(); return true; case gb_Input.SelectSlot: mWheelController.reset(); tickIf(mActionMenu.setSelectedIndex((slot == 0) ? 9 : slot - 1)); return true; } return false; }
7.4.7. Process input in none
private ui bool processInputInNone(int input, int slot) { mWheelController.reset(); switch (input) { case gb_Input.SelectPrevWeapon: return processSelectPreviousWeaponInNone(); case gb_Input.SelectNextWeapon: return processSelectNextWeaponInNone(); case gb_Input.SelectSlot: return processSelectSlotInNone(slot); case gb_Input.InventoryPrevious: case gb_Input.InventoryNext: return processInventoryScrollInNone(); } return false; } private ui bool processSelectPreviousWeaponInNone() { int openViewType = mOptions.getOpenOnScroll(); if (openViewType != 0) { openWeapons(openViewType); mWeaponMenu.selectPrevWeapon(); } else { mWeaponMenu.selectPrevWeapon(); gb_Sender.sendSelectEvent(mWeaponMenu.confirmSelection()); } return true; } private ui bool processSelectNextWeaponInNone() { int openViewType = mOptions.getOpenOnScroll(); if (openViewType != 0) { openWeapons(openViewType); mWeaponMenu.selectNextWeapon(); } else { mWeaponMenu.selectNextWeapon(); gb_Sender.sendSelectEvent(mWeaponMenu.confirmSelection()); } return true; } private ui bool processSelectSlotInNone(int slot) { int openViewType = mOptions.getOpenOnSlot(); if (openViewType == 0) { tickIf(mWeaponMenu.selectSlot(slot)); gb_Sender.sendSelectEvent(mWeaponMenu.confirmSelection()); return true; } if (mOptions.isNoMenuIfOne() && mWeaponMenu.isOneWeaponInSlot(slot)) { tickIf(mWeaponMenu.selectSlot(slot)); gb_Sender.sendSelectEvent(mWeaponMenu.confirmSelection()); } else if (mWeaponMenu.selectSlot(slot, mOptions.isSelectFirstSlotWeapon())) { mSounds.playOpen(); mActivity.openWeapons(openViewType); } else { mSounds.playNope(); return false; } return true; } private ui bool processInventoryScrollInNone() { int openViewType = mOptions.getInventoryOpenOnScroll(); if (openViewType != 0) openInventory(openViewType); return false; }
7.4.8. Network process
override void networkProcess(ConsoleEvent event) { if (players[consolePlayer].mo == NULL) return; int input = mNeteventProcessor.process(event); switch (input) { case gb_Input.ResetCustomOrder: resetCustomOrder(); break; } }
7.4.9. Render overlay
override void renderOverlay(RenderEvent event) { if (!mIsInitialized) return; if (!mTextureCache.isLoaded) mTextureCache.load(); mActionMenu.initializeFromUi(); mCaption.show(); mFadeInOut.fadeInOut((mActivity.isNone()) ? -0.1 : 0.2); gb_Blur.setEnabled(mOptions.isBlurEnabled() && !mActivity.isNone()); double alpha = mFadeInOut.getAlpha(); if (mActivity.isNone() && alpha == 0.0) return; gb_ViewModel viewModel; if (mActivity.isWeapons()) mWeaponMenu.fill(viewModel); else if (mActivity.isInventory()) mInventoryMenu.fill(viewModel); else if (mActivity.isActions()) mActionMenu.fill(viewModel); if (!viewModel.isValid()) throwAbortException("ERROR: invalid view model: %s", viewModel.print()); gb_Dim.dim(alpha, mOptions); switch (getActiveViewType()) { case 0: mBlockyView.setAlpha(alpha); mBlockyView.setScale(mOptions.getScale()); mBlockyView.setBaseColor(mOptions.getColor()); mBlockyView.display(viewModel); break; case 1: { gb_WheelControllerModel controllerModel; mWheelController.fill(controllerModel); mWheelIndexer.update(viewModel, controllerModel); int selectedViewIndex = mWheelIndexer.getSelectedIndex(); if (mActivity.isWeapons()) tickIf(mWeaponMenu.setSelectedIndexFromView(viewModel, selectedViewIndex)); else if (mActivity.isInventory()) tickIf(mInventoryMenu.setSelectedIndex(selectedViewIndex)); else if (mActivity.isActions()) tickIf(mActionMenu.setSelectedIndex(selectedViewIndex)); if (selectedViewIndex != -1) viewModel.selectedIndex = selectedViewIndex; int selectedIndex; if (mActivity.isWeapons()) selectedIndex = mWeaponMenu.getSelectedIndex(); else if (mActivity.isInventory()) selectedIndex = mInventoryMenu.getSelectedIndex(); else if (mActivity.isActions()) selectedIndex = mActionMenu.getSelectedIndex(); else throwAbortException("ERROR: unknown activity state"); mWheelView.setAlpha(alpha); mWheelView.setBaseColor(mOptions.getColor()); mWheelView.setRotating(mActivity.isWeapons()); int innerIndex = mWheelIndexer.getInnerIndex(selectedIndex, viewModel); int outerIndex = mWheelIndexer.getOuterIndex(selectedIndex, viewModel); mWheelView.display( viewModel , controllerModel , mOptions.isMouseInWheel() , innerIndex , outerIndex ); break; } case 2: mTextView.setAlpha(alpha); mTextView.setScale(mOptions.getTextScale()); mTextView.display(viewModel); break; } }
7.4.10. Get active view type
private int getActiveViewType() const { if (mActivity.isNone()) return -1; switch (mActivity.getOpenViewType()) { case 1: if (mActivity.isWeapons()) return mOptions.getWeaponsViewType(); else if (mActivity.isInventory()) return mOptions.getInventoryViewType(); else if (mActivity.isActions()) return mOptions.getActionsViewType(); else throwAbortException("ERROR: unknown activity state"); case 2: return 0; case 3: return 1; case 4: return 2; } throwAbortException("ERROR: unexpected open view type"); return -1; }
7.4.11. Is disabled on automap
private play bool isDisabledOnAutomap() const { return automapActive && !mOptions.isOnAutomap(); }
7.4.12. Is player frozen
private play bool isPlayerFrozen() const { return players[consolePlayer].isTotallyFrozen() && !mOptions.isFrozenCanOpen(); }
7.4.13. Tick if
private ui void tickIf(bool mustTick) { if (mustTick) mSounds.playTick(); }
7.4.14. Open weapons
private ui void openWeapons(int openViewType) { if (isDead() || mWeaponMenu.isThereNoWeapons()) { mSounds.playNope(); return; } mWeaponMenu.setSelectedWeapon(gb_WeaponWatcher.current()); mSounds.playOpen(); mActivity.openWeapons(openViewType); }
7.4.15. Open inventory
private ui void openInventory(int openViewType) { if (isDead() || (gb_InventoryMenu.getItemsNumber() == 0)) { mSounds.playNope(); return; } mSounds.playOpen(); mActivity.openInventory(openViewType); }
7.4.16. Open actions
private ui void openActions(int openViewType) { if (isDead()) { mSounds.playNope(); return; } mWeaponMenu.setSelectedIndex(mActionMenu.getSelectedIndex()); mSounds.playOpen(); mActivity.openActions(openViewType); }
7.4.17. Is dead
private clearscope static bool isDead() { return (players[consolePlayer].mo.health <= 0); }
7.4.18. Close
private clearscope void close() { mSounds.playClose(); mActivity.close(); }
7.4.19. Confirm selection
private ui void confirmSelection() { if (mActivity.isWeapons()) gb_Sender.sendSelectEvent(mWeaponMenu.confirmSelection()); else if (mActivity.isInventory()) gb_Sender.sendUseItemEvent(mInventoryMenu.confirmSelection()); else if (mActivity.isActions()) gb_Sender.sendEvent(mActionMenu.confirmSelection()); }
7.4.20. Drop item
private ui void dropItem() { if (paused) { return; } if (mActivity.isWeapons()) { gb_Sender.sendDropItemEvent(mWeaponMenu.confirmSelection()); mSounds.playTick(); } else if (mActivity.isInventory()) { gb_Sender.sendDropItemEvent(mInventoryMenu.confirmSelection()); mSounds.playTick(); } }
7.4.21. Rotate weapon priority
private ui void rotateWeaponPriority() { if (mActivity.isWeapons()) { gb_CustomWeaponOrderStorage .savePriorityRotation(mWeaponSetHash, mWeaponMenu.getSelectedIndex()); mWeaponMenu.rotatePriority(); } }
7.4.22. Rotate weapon slot
private ui void rotateWeaponSlot() { if (mActivity.isWeapons()) { gb_CustomWeaponOrderStorage .saveSlotRotation(mWeaponSetHash, mWeaponMenu.getSelectedIndex()); mWeaponMenu.rotateSlot(); } }
7.4.23. Reset custom order
private void resetCustomOrder() { gb_CustomWeaponOrderStorage.reset(mWeaponSetHash); gb_WeaponData weaponData; gb_WeaponDataLoader.load(weaponData, !mOptions.isSlotCycleOrderReversed()); mWeaponMenu = gb_WeaponMenu.from(weaponData, mOptions); }
7.4.24. Initialize
private void initialize() { mOptions = gb_Options.from(); mFontSelector = gb_FontSelector.from(); mSounds = gb_Sounds.from(mOptions); gb_WeaponData weaponData; gb_WeaponDataLoader.load(weaponData, !mOptions.isSlotCycleOrderReversed()); mWeaponSetHash = gb_CustomWeaponOrderStorage.calculateHash(weaponData); mWeaponMenu = gb_WeaponMenu.from(weaponData, mOptions); gb_CustomWeaponOrderStorage.applyOperations(mWeaponSetHash, mWeaponMenu); mInventoryMenu = gb_InventoryMenu.from(mOptions); mActionMenu = gb_ActionMenu.from(); mActivity = gb_Activity.from(); mFadeInOut = gb_FadeInOut.from(); mFreezer = gb_Freezer.from(mOptions); mTextureCache = gb_TextureCache.from(); mCaption = gb_Caption.from(); mInventoryUser = gb_InventoryUser.from(); mChanger = gb_Changer.from(mCaption, mOptions, mInventoryUser); mNeteventProcessor = gb_NeteventProcessor.from(mChanger); mBlockyView = gb_BlockyView.from(mTextureCache, mOptions, mFontSelector); mTextView = gb_TextView.from(mOptions, mFontSelector); mMultiWheelMode = gb_MultiWheelMode.from(mOptions); let screen = gb_Screen.from(mOptions); mWheelView = gb_WheelView.from( mOptions , mMultiWheelMode , mTextureCache , screen , mFontSelector ); mWheelController = gb_WheelController.from(mOptions, screen); mWheelIndexer = gb_WheelIndexer.from(mMultiWheelMode, screen); mIsInitialized = true; }
7.4.25. Variables
private gb_Options mOptions; private gb_FontSelector mFontSelector; private gb_Sounds mSounds; private string mWeaponSetHash; private gb_WeaponMenu mWeaponMenu; private gb_InventoryMenu mInventoryMenu; private gb_ActionMenu mActionMenu; private gb_Activity mActivity; private gb_FadeInOut mFadeInOut; private gb_Freezer mFreezer; private gb_TextureCache mTextureCache; private gb_Caption mCaption; private gb_InventoryUser mInventoryUser; private gb_Changer mChanger; private gb_NeteventProcessor mNeteventProcessor; private gb_BlockyView mBlockyView; private gb_TextView mTextView; private gb_MultiWheelMode mMultiWheelMode; private gb_WheelView mWheelView; private gb_WheelController mWheelController; private gb_WheelIndexer mWheelIndexer; private bool mIsInitialized;
7.5. Service
7.5.1. Service
// This is Service interface. class gb_Service abstract { virtual play string get(string request) { return ""; } virtual ui string uiGet(string request) { return ""; } } // Use this class to find and iterate over services. // // Example usage: // // ServiceIterator i = ServiceIterator.find("MyService"); // Service s; // while (s = i.next()) // { // string request = ... // string answer = s.get(request); // ... // } // // If no services are found, the all calls to next() will return NULL. class gb_ServiceIterator { // Creates a Service iterator for a service name. It will iterate over all // existing Services with names that match @a serviceName or have it as a part // of their names. // // Matching is case-independent. // // serviceName: class name of service to find. static gb_ServiceIterator find(string serviceName) { let result = new("gb_ServiceIterator"); result.mServiceName = serviceName; result.mClassIndex = 0; result.findNextService(); return result; } // Gets the service and advances the iterator. // // Returns service instance, or NULL if no more servers found. // // Note: each ServiceIterator will return new instances of services. gb_Service next() { int classesNumber = AllClasses.size(); gb_Service result = (mClassIndex == classesNumber) ? NULL : gb_Service(new(AllClasses[mClassIndex])); ++mClassIndex; findNextService(); return result; } private void findNextService() { int classesNumber = AllClasses.size(); while (mClassIndex < classesNumber && !ServiceNameContains(AllClasses[mClassIndex], mServiceName)) { ++mClassIndex; } } private static bool serviceNameContains(class aClass, string substring) { if (!(aClass is "gb_Service")) return false; string className = aClass.getClassName(); string lowerClassName = className.makeLower(); string lowerSubstring = substring.makeLower(); bool result = lowerClassName.indexOf(lowerSubstring) != -1; return result; } private string mServiceName; private int mClassIndex; }
7.5.2. Icon service
class gb_IconService : gb_Service { override string uiGet(string className) { if (isSmoothDoom()) switch (Name(className)) { case 'PerkFist' : return "PUNGB0"; case 'Z86Chainsaw' : return "CSAWA0"; case 'PerkShotgun' : return "SHOTA0"; case 'PerkSuperShotgun' : return "SGN2A0"; case 'Z86Chaingun' : return "MGUNA0"; case 'PerkRocketLauncher' : return "LAUNA0"; case 'BloxPlasmaRifle' : return "PLRLA0"; case 'Z86BFG9000' : return "BFG9A0"; } switch (Name(className)) { case 'Fist' : return "SMFIST0"; case 'Chainsaw' : return "SMCSAW0"; case 'Pistol' : return "SMPISG0"; case 'Shotgun' : return "SMSHOT0"; case 'SuperShotgun' : return "SMSGN20"; case 'Chaingun' : return "SMMGUN0"; case 'RocketLauncher' : return "SMLAUN0"; case 'PlasmaRifle' : return "SMPLAS0"; case 'BFG9000' : return "SMBFGG0"; default: return IGNORE; } } const IGNORE = ""; private static bool isSmoothDoom() { string s1 = "SmoothFloatingSkull"; class c1 = s1; string s2 = "SmoothDoomImp"; class c2 = s2; return (c1 && c2); } }
7.5.3. Hide service
class gb_HideService : gb_Service { override string get(string className) { switch (Name(className)) { // SWWM GZ: https://forum.zdoom.org/viewtopic.php?f=43&t=67687 case 'DualExplodiumGun': { string explodiumGunClass = "ExplodiumGun"; return (players[consolePlayer].mo.countInv(explodiumGunClass) > 1) ? SHOW : HIDE; } default: return IGNORE; } } const HIDE = "1"; const SHOW = "0"; const IGNORE = ""; }
7.6. Tools
7.6.1. Cvar
// This class provides access to a user or server Cvar. // // Accessing Cvars through this class is faster because calling Cvar.GetCvar() // is costly. This class caches the result of Cvar.GetCvar() and handles // loading a savegame. class gb_Cvar { static gb_Cvar from(string name) { let result = new("gb_Cvar"); result.mName = name; result.load(); return result; } string getString() { if (!mCvar) load(); return mCvar.getString(); } bool getBool() { if (!mCvar) load(); return mCvar.getInt(); } int getInt() { if (!mCvar) load(); return mCvar.getInt(); } double getDouble() { if (!mCvar) load(); return mCvar.getFloat(); } private void load() { mCvar = Cvar.getCvar(mName, players[consolePlayer]); if (mCvar == NULL) gb_Log.error(string.format("cvar %s not found", mName)); } private string mName; private transient Cvar mCvar; }
7.6.2. Log
class gb_Log { static void print(string s) { Console.printf("%s", StringTable.localize(s, false)); } static void notice(string s) { Console.printf("[NOTICE] %s: %s", MOD_NAME, StringTable.localize(s, false)); } static void error(string s) { Console.printf("[ERROR] %s: %s.", MOD_NAME, s); } static void log(string s) { Console.printf("[LOG] %s: %s.", MOD_NAME, s); } static void debug(string s) { if (DEBUG_ENABLED) { Console.printf("[DEBUG] %s: %s.", MOD_NAME, s); } } const DEBUG_ENABLED = 0; // == false const MOD_NAME = "Gearbox"; }
7.6.3. Texture cache
class gb_TextureCache { static gb_TextureCache from() { return new("gb_TextureCache"); } void load() { isLoaded = true; // Wheel circle = loadTexture("gb_circ"); halfCircle = loadTexture("gb_hcir"); ammoPip = loadTexture("gb_pip"); hand = loadTexture("gb_hand"); pointer = loadTexture("gb_pntr"); textBox = loadTexture("gb_desc"); // Blocks blockBox = loadTexture("gb_box"); blockBig = loadTexture("gb_weap"); corner = loadTexture("gb_cor"); ammoLine = loadTexture("gb_ammo"); // Sizes ammoPipSize = TexMan.getScaledSize(ammoPip); } private TextureID loadTexture(string name) { return TexMan.checkForTexture(name, TexMan.Type_MiscPatch, 0); } transient TextureID circle; transient TextureID halfCircle; transient TextureID ammoPip; transient TextureID hand; transient TextureID pointer; transient TextureID textBox; transient TextureID blockBox; transient TextureID blockBig; transient TextureID corner; transient TextureID ammoLine; transient vector2 ammoPipSize; transient bool isLoaded; }
7.7. Wheel
7.7.1. Controller
class gb_WheelController { static gb_WheelController from(gb_Options options, gb_Screen screen) { let result = new("gb_WheelController"); result.reset(); result.mScreen = screen; result.mOptions = options; return result; } void reset() { mX = 0; mY = 0; PlayerInfo player = players[consolePlayer]; mStartPitch = player.mo.pitch; mStartYaw = player.mo.angle; } void fill(out gb_WheelControllerModel model) { model.angle = getAngle(); model.radius = getRadius(); } play void process() const { if (!mOptions.isMouseInWheel()) return; vector2 mouseSensitivity = mOptions.getMouseSensitivity(); double yawMod = 0.08 * mouseSensitivity.x; double pitchMod = 0.08 * mouseSensitivity.y; PlayerInfo player = players[consolePlayer]; // Code derived from PyWeaponWheel's mouse input handling. mX -= int(round(player.original_cmd.yaw * yawMod )); mY -= int(round(player.original_cmd.pitch * pitchMod)); if (multiplayer) { gb_Sender.sendPlayerAngles(mStartPitch, mStartYaw); } else { gb_Changer.setAngles(players[consolePlayer], mStartPitch, mStartYaw); } vector2 center = mScreen.getWheelCenter(); int centerX = int(center.x); int centerY = int(center.y); mX = clamp(mX, -centerX, Screen.getWidth() - centerX); mY = clamp(mY, -centerY, Screen.getHeight() - centerY); } private double getAngle() const { return -atan2(mX, mY) + 180; } private double getRadius() const { return sqrt(mX * mX + mY * mY); } private int mX; private int mY; private double mStartPitch; private double mStartYaw; private gb_Screen mScreen; private gb_Options mOptions; }
7.7.2. Wheel controller model
struct gb_WheelControllerModel { double angle; double radius; int getSelectedIndex(int nItems, gb_Screen screen) const { if (radius < screen.getWheelDeadRadius() || nItems == 0) return -1; int result = int(round(angle * nItems / 360.0)) % nItems; return result; } }
7.7.3. Wheel indexer
class gb_WheelIndexer { static gb_WheelIndexer from(gb_MultiWheelMode multiWheelMode, gb_Screen screen) { let result = new("gb_WheelIndexer"); result.mSelectedIndex = UNDEFINED_INDEX; result.mLastSlotIndex = UNDEFINED_INDEX; result.mInnerIndex = UNDEFINED_INDEX; result.mOuterIndex = UNDEFINED_INDEX; result.mMultiWheelMode = multiWheelMode; result.mScreen = screen; return result; } int getSelectedIndex() const { return mSelectedIndex; } int getInnerIndex(int externalSelectedIndex, gb_ViewModel viewModel) const { if (areIndicesDefined()) return mInnerIndex; int nWeapons = viewModel.tags.size(); int externalSelectedIndexInModel = UNDEFINED_INDEX; for (int i = 0; i < nWeapons; ++i) { if (viewModel.indices[i] == externalSelectedIndex) { externalSelectedIndexInModel = i; break; } } if (externalSelectedIndexInModel < 0 || !mMultiWheelMode.isEngaged(viewModel)) return externalSelectedIndexInModel; gb_MultiWheelModel multiWheelModel; gb_MultiWheel.fill(viewModel, multiWheelModel); int nPlaces = multiWheelModel.data.size(); for (int i = 0; i < nPlaces; ++i) { if (multiWheelModel.isWeapon[i]) { if (viewModel.indices[multiWheelModel.data[i]] == externalSelectedIndex) return i; } else { if (multiWheelModel.data[i] == viewModel.slots[externalSelectedIndexInModel]) return i; } } return UNDEFINED_INDEX; } int getOuterIndex(int externalSelectedIndex, gb_ViewModel viewModel) const { if (areIndicesDefined()) return mOuterIndex; if (!mMultiWheelMode.isEngaged(viewModel)) return UNDEFINED_INDEX; int nWeapons = viewModel.tags.size(); int externalSelectedIndexInModel = UNDEFINED_INDEX; for (int i = 0; i < nWeapons; ++i) { if (viewModel.indices[i] == externalSelectedIndex) { externalSelectedIndexInModel = i; break; } } if (externalSelectedIndexInModel < 0) return externalSelectedIndexInModel; int slot = viewModel.slots[externalSelectedIndexInModel]; int start = 0; for (; start < nWeapons && viewModel.slots[start] != slot; ++start); return externalSelectedIndexInModel - start; } void update(gb_ViewModel viewModel, gb_WheelControllerModel controllerModel) { if (controllerModel.radius < mScreen.getWheelDeadRadius()) { mSelectedIndex = UNDEFINED_INDEX; mLastSlotIndex = UNDEFINED_INDEX; mInnerIndex = UNDEFINED_INDEX; mOuterIndex = UNDEFINED_INDEX; return; } int nWeapons = viewModel.tags.size(); if (!mMultiWheelMode.isEngaged(viewModel)) { mSelectedIndex = controllerModel.getSelectedIndex(nWeapons, mScreen); mLastSlotIndex = UNDEFINED_INDEX; mInnerIndex = mSelectedIndex; mOuterIndex = UNDEFINED_INDEX; return; } gb_MultiWheelModel multiWheelModel; gb_MultiWheel.fill(viewModel, multiWheelModel); int nPlaces = multiWheelModel.data.size(); int wheelRadius = mScreen.getWheelRadius(); if (controllerModel.radius < wheelRadius) { reportInnerIndex(nPlaces, controllerModel, multiWheelModel, viewModel); return; } if (mLastSlotIndex == UNDEFINED_INDEX || mLastSlotIndex >= multiWheelModel.data.size()) { mSelectedIndex = UNDEFINED_INDEX; mInnerIndex = UNDEFINED_INDEX; mOuterIndex = UNDEFINED_INDEX; return; } int slot = multiWheelModel.data[mLastSlotIndex]; int start = 0; for (; start < nWeapons && viewModel.slots[start] != slot; ++start); int end = start; for (; end < nWeapons && viewModel.slots[end] == slot; ++end); int nWeaponsInSlot = end - start; double slotAngle = itemAngle(nPlaces, mLastSlotIndex); double r = controllerModel.radius; double w = wheelRadius; double forSlotAngle = slotAngle - controllerModel.angle; double side = sqrt(r * r + w * w - 2 * r * w * cos(forSlotAngle)); if (side < mScreen.getWheelDeadRadius()) { reportInnerIndex(nPlaces, controllerModel, multiWheelModel, viewModel); return; } // Due to computation precision, the value of ratio may be // slightly out of range [-1, 1]. double ratio = clamp(r / side * sin(forSlotAngle), -1.0, 1.0); double angle = -asin(ratio); // Limit angle on the borders of the second wheel: double borderRadius = wheelRadius / sin(90 - forSlotAngle); if (r < borderRadius) { angle = (angle > 0) ? 89.9999 : -89.9999; } angle += 90; angle %= 180; int indexInSlot = int((angle * nWeaponsInSlot / 180.0) % nWeaponsInSlot); mSelectedIndex = start + indexInSlot; mInnerIndex = mLastSlotIndex; mOuterIndex = indexInSlot; } private void reportInnerIndex( int nPlaces , gb_WheelControllerModel controllerModel , gb_MultiWheelModel multiWheelModel , gb_ViewModel viewModel ) { int innerIndex = controllerModel.getSelectedIndex(nPlaces, mScreen); bool isWeapon = multiWheelModel.isWeapon[innerIndex]; mSelectedIndex = isWeapon ? multiWheelModel.data[innerIndex] : firstInSlot(viewModel, multiWheelModel.data[innerIndex]); mLastSlotIndex = isWeapon ? UNDEFINED_INDEX : innerIndex; mInnerIndex = innerIndex; mOuterIndex = 0; } private static int firstInSlot(gb_ViewModel viewModel, int slot) { int nWeapons = viewModel.tags.size(); for (int i = 0; i < nWeapons; ++i) { if (viewModel.slots[i] == slot) return i; } return UNDEFINED_INDEX; } private bool areIndicesDefined() const { return (mInnerIndex != UNDEFINED_INDEX); } private static double itemAngle(int nItems, int index) { return 360.0 / nItems * index; } const UNDEFINED_INDEX = -1; private int mSelectedIndex; private int mLastSlotIndex; private int mInnerIndex; private int mOuterIndex; private gb_MultiWheelMode mMultiWheelMode; private gb_Screen mScreen; }
7.7.4. Multi wheel
struct gb_MultiWheelModel { Array<bool> isWeapon; Array<int> data; // slot or weapon index } class gb_MultiWheel { static void fill(gb_ViewModel viewModel, out gb_MultiWheelModel model) { int nWeapons = viewModel.tags.size(); int lastSlot = -1; for (int i = 0; i < nWeapons; ++i) { int slot = viewModel.slots[i]; bool isSingle = isSingleWeaponInSlot(viewModel, nWeapons, slot); if (isSingle) { model.isWeapon.push(true); model.data.push(i); } else { if (slot != lastSlot) { lastSlot = slot; model.isWeapon.push(false); model.data.push(slot); } } } } static bool isSingleWeaponInSlot(gb_ViewModel viewModel, int nWeapons, int slot) { int nWeaponsInSlot = 0; for (int i = 0; i < nWeapons; ++i) { nWeaponsInSlot += (viewModel.slots[i] == slot); if (nWeaponsInSlot > 1) return false; } return true; } }
7.7.5. Multi wheel mode
class gb_MultiWheelMode { static gb_MultiWheelMode from(gb_Options options) { let result = new("gb_MultiWheelMode"); result.mOptions = options; return result; } bool isEngaged(gb_ViewModel viewModel) { int nWeapons = viewModel.tags.size(); bool result = (nWeapons > mOptions.getMultiWheelLimit()); return result; } private gb_Options mOptions; }
7.7.6. Screen
// This class provides helper functions for screen position and sizes. class gb_Screen { static gb_Screen from(gb_Options options) { let result = new("gb_Screen"); result.mOptions = options; return result; } vector2 getWheelCenter() const { int screenWidth = Screen.getWidth(); int halfScreenHeight = Screen.getHeight() / 2; int centerPosition = int((screenWidth / 2 - halfScreenHeight) * mOptions.getWheelPosition()); return (screenWidth / 2 + centerPosition, halfScreenHeight); } int getWheelRadius() const { return getScaledScreenHeight() / 4; } int getWheelDeadRadius() const { return getScaledScreenHeight() / 16; } double getScaleFactor() const { return getScaledScreenHeight() / 1080.0; } int getScaledScreenHeight() const { return int(Screen.getHeight() * mOptions.getWheelScale()); } int getScaledScreenWidth() const { return int(Screen.getWidth() * mOptions.getWheelScale()); } private gb_Options mOptions; }
7.7.7. Text
// This class helps displaying text on screen. class gb_Text { static gb_Text from(gb_TextureCache textureCache, gb_Screen screen, gb_FontSelector fontSelector) { let result = new("gb_Text"); result.mTextureCache = textureCache; result.mScreen = screen; result.mFontSelector = fontSelector; return result; } const HIGHLIGHT_FACTOR = 1.2; static void draw(string aString, vector2 pos, Font aFont, double alpha, bool isBig = false, int color = Font.CR_WHITE, bool isHighlighted = false) { int textScale = isBig ? getBigTextScale() : getTextScale(); if (isHighlighted) { double scale = textScale * HIGHLIGHT_FACTOR; double x = pos.x - aFont.stringWidth(aString) * scale / 2; double y = pos.y - aFont.getHeight() * scale / 2; Screen.drawText( aFont , Font.CR_YELLOW , x , y , aString , DTA_Alpha , alpha , DTA_ScaleX , scale , DTA_ScaleY , scale , DTA_FillColor, Color(0xFFFF00) ); } double x = pos.x - aFont.stringWidth(aString) * textScale / 2; double y = pos.y - aFont.getHeight() * textScale / 2; Screen.drawText( aFont , color , x , y , aString , DTA_Alpha , alpha , DTA_ScaleX , textScale , DTA_ScaleY , textScale ); } void drawBox( string topText , string middleText , string bottomText , vector2 pos , bool isPosTop // true: pos is the top of text box, false: bottom. , color baseColor , double alpha ) { double scaleFactor = mScreen.getScaleFactor(); Font aFont = mFontSelector.getFont(); int textScale = getTextScale(); int lineHeight = aFont.getHeight() * textScale; int margin = int(10 * scaleFactor); int height = int((margin * 2 + lineHeight * 3) / scaleFactor); if (!isPosTop) pos.y -= height * scaleFactor; int topTextWidth = aFont.stringWidth(topText); int middleTextWidth = aFont.stringWidth(middleText); int bottomTextWidth = aFont.stringWidth(bottomText); int width = max(int((max(middleTextWidth, max(topTextWidth, bottomTextWidth)) * textScale + margin * 2) / scaleFactor), height * 2); vector2 size = (width, height) * scaleFactor; vector2 center = pos + (0, size.y / 2); Screen.drawTexture( mTextureCache.textBox , NO_ANIMATION , center.x , center.y , DTA_FillColor , baseColor , DTA_AlphaChannel , true , DTA_CenterOffset , true , DTA_Alpha , alpha , DTA_DestWidth , int(size.x) , DTA_DestHeight , int(size.y) ); vector2 line = (0, lineHeight); if (topText .length() > 0) draw(topText , center - line, aFont, alpha); if (middleText.length() > 0) draw(middleText, center , aFont, alpha); if (bottomText.length() > 0) draw(bottomText, center + line, aFont, alpha); } private static int getBigTextScale() { return getTextScale() * 2; } private static int getTextScale() { return max(Screen.getHeight() / 720, 1); } const NO_ANIMATION = 0; // == false private gb_TextureCache mTextureCache; private gb_Screen mScreen; private gb_FontSelector mFontSelector; }
7.8. Wheel view
class gb_WheelView
7.8.1. Constructor
static gb_WheelView from(gb_Options options, gb_MultiWheelMode multiWheelMode, gb_TextureCache textureCache, gb_Screen screen, gb_FontSelector fontSelector) { let result = new("gb_WheelView"); result.setAlpha(1.0); result.setBaseColor(0x2222CC); result.mScreen = screen; result.mOptions = options; result.mFontSelector = fontSelector; result.mMultiWheelMode = multiWheelMode; result.mText = gb_Text.from(textureCache, screen, fontSelector); result.mTextureCache = textureCache; result.mIsRotating = true; return result; }
7.8.2. Setters
void setAlpha (double alpha) { mAlpha = alpha; } void setBaseColor(int color) { mBaseColor = color; } void setRotating (bool isRotating) { mIsRotating = isRotating; }
7.8.3. Display
void display(gb_ViewModel viewModel, gb_WheelControllerModel controllerModel, bool showPointer, int innerIndex, int outerIndex) const { mScaleFactor = mScreen.getScaleFactor(); mCenter = mScreen.getWheelCenter(); drawInnerWheel(); int nWeapons = viewModel.tags.size(); if (nWeapons == 0) return; int screenHeight = mScreen.getScaledScreenHeight(); int radius = screenHeight * 5 / 32; int allowedWidth = int(screenHeight * 3 / 16 - MARGIN * 2 * mScaleFactor); int nPlaces = 0; bool isSlotExpanded = false; bool isMultiWheelMode = mMultiWheelMode.isEngaged(viewModel); if (isMultiWheelMode) { gb_MultiWheelModel multiWheelModel; gb_MultiWheel.fill(viewModel, multiWheelModel); nPlaces = multiWheelModel.data.size(); for (int i = 0; i < nPlaces; ++i) { bool isWeapon = multiWheelModel.isWeapon[i]; int data = multiWheelModel.data[i]; if (isWeapon) displayWeapon(i, data, nPlaces, radius, allowedWidth, viewModel, mCenter); else displaySlot(i, data, nPlaces, radius); } drawHands(nPlaces, innerIndex, mCenter, 0); isSlotExpanded = (outerIndex != UNDEFINED_INDEX && !multiWheelModel.isWeapon[innerIndex]); if (isSlotExpanded) { int slot = multiWheelModel.data[innerIndex]; drawOuterWeapons( innerIndex , outerIndex , nPlaces , slot , viewModel , radius , allowedWidth , int(controllerModel.radius) ); } } else { isSlotExpanded = false; for (int i = 0; i < nWeapons; ++i) { if (i == innerIndex) continue; displayWeapon(i, i, nWeapons, radius, allowedWidth, viewModel, mCenter); } nPlaces = nWeapons; if (innerIndex != -1) { displayWeapon(innerIndex, innerIndex, nPlaces, radius, allowedWidth, viewModel, mCenter, 0.0, true); } drawHands(nPlaces, innerIndex, mCenter, 0); } if (showPointer) { drawPointer(controllerModel.angle, controllerModel.radius); } int showingTags = mOptions.getShowingTags(); if (showingTags != 0) { drawWeaponDescription(viewModel, innerIndex, nPlaces, isSlotExpanded, showingTags == 1); } } private void displaySlot(int place, int slot, int nPlaces, int radius) { double angle = itemAngle(nPlaces, place); vector2 pos = (sin(angle), -cos(angle)) * radius + mCenter; Font aFont = mFontSelector.getFont(); gb_Text.draw(string.format("%d", slot), pos, aFont, mAlpha, true); }
7.8.4. Draw outer weapons
private void drawOuterWeapons(int innerIndex, int outerIndex, int nPlaces, int slot, gb_ViewModel viewModel, int radius, int allowedWidth, int controllerRadius) { int wheelRadius = mScreen.getWheelRadius(); double angle = itemAngle(nPlaces, innerIndex); vector2 outerWheelCenter = (sin(angle), -cos(angle)) * wheelRadius + mCenter; drawOuterWheel(outerWheelCenter.x, outerWheelCenter.y, -angle); int nWeapons = viewModel.tags.size(); int start = 0; for (; start < nWeapons && viewModel.slots[start] != slot; ++start); int end = start; for (; end < nWeapons && viewModel.slots[end] == slot; ++end); int nWeaponsInSlot = end - start; double startingAngle = angle - 90 + (180.0 / nWeaponsInSlot / 2); int place = 0; int selectedPlace = 0; for (int i = start; i < end; ++i, ++place) { if (i == viewModel.selectedIndex) { selectedPlace = place; continue; } displayWeapon( place , i , nWeaponsInSlot * 2 , radius , allowedWidth , viewModel , outerWheelCenter , startingAngle ); } // draw the selected thing last in case of icon overlapping displayWeapon(selectedPlace, viewModel.selectedIndex, nWeaponsInSlot * 2, radius, allowedWidth, viewModel, outerWheelCenter, startingAngle, true); drawHands(nWeaponsInSlot * 2, outerIndex, outerWheelCenter, -startingAngle); }
7.8.5. Drawing wheels
private void drawInnerWheel() { int wheelDiameter = mScreen.getWheelRadius() * 2; Screen.drawTexture( mTextureCache.circle , NO_ANIMATION , mCenter.x , mCenter.y , DTA_FillColor , mBaseColor , DTA_AlphaChannel , true , DTA_Alpha , mAlpha , DTA_CenterOffset , true , DTA_DestWidth , wheelDiameter , DTA_DestHeight , wheelDiameter ); } private void drawOuterWheel(double x, double y, double angle) { int wheelDiameter = mScreen.getWheelRadius() * 2; Screen.drawTexture( mTextureCache.halfCircle , NO_ANIMATION , x , y , DTA_FillColor , mBaseColor , DTA_AlphaChannel , true , DTA_Alpha , mAlpha , DTA_CenterOffset , true , DTA_Rotate , angle , DTA_DestWidth , wheelDiameter , DTA_DestHeight , wheelDiameter ); }
7.8.6. Display weapon
Code is adapted from GZDoom AltHud.DrawImageToBox.
private void displayWeapon(int place, int iconIndex, int nPlaces, int radius, int allowedWidth, gb_ViewModel viewModel, vector2 center, double startingAngle = 0.0, bool isHighlighted = false) const { TextureID texture = viewModel.icons[iconIndex]; double angle = (startingAngle + itemAngle(nPlaces, place)) % 360; vector2 xy = (sin(angle), -cos(angle)) * radius + center; if (texture.isValid()) { vector2 textureSize = TexMan.getScaledSize(texture) * 2 * mScaleFactor; textureSize.x *= viewModel.iconScaleXs[iconIndex]; textureSize.y *= viewModel.iconScaleYs[iconIndex]; bool isTall = (textureSize.y * 1.2 > textureSize.x); double scale = isTall ? ((allowedWidth < textureSize.y) ? allowedWidth / textureSize.y : 1.0) : ((allowedWidth < textureSize.x) ? allowedWidth / textureSize.x : 1.0); vector2 size = textureSize * scale; drawIcon(texture, xy, size, angle, isTall, isHighlighted); } else { drawIconSubstitute(viewModel.iconSubstitutes[iconIndex], viewModel.iconSubstituteColors[iconIndex], xy, isHighlighted); } drawAmmo(angle, center, viewModel, iconIndex); }
7.8.7. Draw icon
private void drawIcon(TextureID texture, vector2 xy, vector2 size, double angle, bool isTall, bool isHighlighted) const { bool flipX; double scaleY; if (mIsRotating) { flipX = (angle > 180); if (flipX) angle -= 180; angle = -angle + 90; if (isTall) angle += flipX ? 90 : -90; scaleY = 1.0; } else { flipX = false; angle = 0; scaleY = mOptions.isPreservingAspectRatio() ? 1.2 : 1.0; } if (isHighlighted) { int highlightColor = Color(0xFFFF00); Screen.drawTexture( texture , NO_ANIMATION , xy.x , xy.y , DTA_CenterOffset , true , DTA_DestWidthF , size.x * 1.1 , DTA_DestHeightF , size.y * 1.1 , DTA_ScaleY , scaleY , DTA_FillColor , highlightColor , DTA_Rotate , angle , DTA_FlipX , flipX ); } Screen.drawTexture( texture , NO_ANIMATION , xy.x , xy.y , DTA_CenterOffset , true , DTA_DestWidthF , size.x , DTA_DestHeightF , size.y , DTA_ScaleY , scaleY , DTA_Alpha , mAlpha , DTA_Rotate , angle , DTA_FlipX , flipX , DTA_KeepRatio, false ); if (!mOptions.getWheelTint()) return; Screen.drawTexture( texture , NO_ANIMATION , xy.x , xy.y , DTA_CenterOffset , true , DTA_DestWidthF , size.x , DTA_DestHeightF , size.y , DTA_ScaleY , scaleY , DTA_Alpha , mAlpha * 0.3 , DTA_FillColor , mBaseColor , DTA_Rotate , angle , DTA_FlipX , flipX ); }
7.8.8. Draw icon substitute
private void drawIconSubstitute(string text, int color, vector2 xy, bool isHighlighted) { Font aFont = mFontSelector.getFont(); gb_Text.draw(text, xy, aFont, mAlpha, true, color, isHighlighted); }
7.8.9. Draw ammo pip
private void drawAmmoPip(double angle, double radius, vector2 center, bool colored) { vector2 xy = (sin(angle), -cos(angle)) * radius + center; vector2 size = mTextureCache.ammoPipSize * mScaleFactor; if (colored) { Screen.drawTexture( mTextureCache.ammoPip , NO_ANIMATION , round(xy.x) , round(xy.y) , DTA_CenterOffset , true , DTA_Alpha , mAlpha , DTA_DestWidth , int(size.x) , DTA_DestHeight , int(size.y) , DTA_FillColor , FILLED_QUANTITY_COLOR ); } else { Screen.drawTexture( mTextureCache.ammoPip , NO_ANIMATION , round(xy.x) , round(xy.y) , DTA_CenterOffset , true , DTA_Alpha , mAlpha , DTA_DestWidth , int(size.x) , DTA_DestHeight , int(size.y) ); } }
7.8.10. Draw ammo
private static int, int makePipsNumbers(int items, int maxItems) { if (maxItems <= MAX_N_PIPS) return items, maxItems; int nColoredPips = int(ceil(MAX_N_PIPS * double(items) / maxItems)); return nColoredPips, MAX_N_PIPS; } private void drawAmmo(double weaponAngle, vector2 center, gb_ViewModel viewModel, int weaponIndex) { if (gb_ViewModel.isAmmoValid(viewModel.quantity1[weaponIndex], viewModel.maxQuantity1[weaponIndex])) { int margin = int(10 * mScaleFactor); int radius = mScreen.getScaledScreenHeight() / 4 - margin; int nColoredPips, nTotalPips; [nColoredPips, nTotalPips] = makePipsNumbers(viewModel.quantity1 [weaponIndex], viewModel.maxQuantity1[weaponIndex]); drawQuantityPips(radius, weaponAngle, center, nColoredPips, nTotalPips); } if (gb_ViewModel.isAmmoValid(viewModel.quantity2[weaponIndex], viewModel.maxQuantity2[weaponIndex])) { int margin = int(20 * mScaleFactor); int radius = mScreen.getScaledScreenHeight() / 4 - margin; int nColoredPips, nTotalPips; [nColoredPips, nTotalPips] = makePipsNumbers(viewModel.quantity2 [weaponIndex], viewModel.maxQuantity2[weaponIndex]); drawQuantityPips(radius, weaponAngle, center, nColoredPips, nTotalPips); } }
7.8.11. Draw quantity pips
void drawQuantityPips(double radius, double centerAngle, vector2 center, int nColoredPips, int nTotalPips) { if (nTotalPips % 2 == 0) { int nTotalPipsHalved = nTotalPips / 2; for (int i = -nTotalPipsHalved + 1; i <= 0; ++i) { double angle = centerAngle - PIPS_GAP + i * PIPS_STEP; drawAmmoPip(angle, radius, center, nColoredPips > 0); --nColoredPips; } for (int i = 0; i < nTotalPipsHalved; ++i) { double angle = centerAngle + PIPS_GAP + i * PIPS_STEP; drawAmmoPip(angle, radius, center, nColoredPips > 0); --nColoredPips; } } else { double angleStart = centerAngle - (nTotalPips - 1) * PIPS_STEP / 2; for (int i = 0; i < nTotalPips; ++i) { double angle = angleStart + i * PIPS_STEP; drawAmmoPip(angle, radius, center, nColoredPips > 0); --nColoredPips; } } }
7.8.12. Item angle
private static double itemAngle(int nItems, int index) { return 360.0 / nItems * index; }
7.8.13. Draw hands
private void drawHands(int nPlaces, int selectedIndex, vector2 center, double startAngle) { if (nPlaces < 2) return; double handsAngle = startAngle - itemAngle(nPlaces, selectedIndex); double sectorAngleHalfWidth = max(6, 360.0 / 2.0 / nPlaces - 2); double scaleFactor = mScreen.getScaleFactor(); vector2 size = TexMan.getScaledSize(mTextureCache.hand) * scaleFactor; Screen.drawTexture( mTextureCache.hand , NO_ANIMATION , center.x , center.y , DTA_KeepRatio , true , DTA_CenterOffset , true , DTA_Alpha , mAlpha , DTA_Rotate , handsAngle - sectorAngleHalfWidth , DTA_FlipX , true , DTA_DestWidthF , size.x , DTA_DestHeightF , size.y ); Screen.drawTexture( mTextureCache.hand , NO_ANIMATION , center.x , center.y , DTA_KeepRatio , true , DTA_CenterOffset , true , DTA_CenterOffset , true , DTA_Alpha , mAlpha , DTA_Rotate , handsAngle + sectorAngleHalfWidth , DTA_DestWidthF , size.x , DTA_DestHeightF , size.y ); }
7.8.14. Draw weapon description
private void drawWeaponDescription(gb_ViewModel viewModel, int innerIndex, int nPlaces, bool isSlotExpanded, bool isTagOrClass) { int index = viewModel.selectedIndex; string description = isTagOrClass ? viewModel.tags[index] : viewModel.classes[index]; if (description.length() == 0) return; string ammo1 = gb_ViewModel.isAmmoValid(viewModel.quantity1[index], viewModel.maxQuantity1[index]) ? string.format("%d/%d", viewModel.quantity1[index], viewModel.maxQuantity1[index]) : ""; string ammo2 = gb_ViewModel.isAmmoValid(viewModel.quantity2[index], viewModel.maxQuantity2[index]) ? string.format("%d/%d", viewModel.quantity2[index], viewModel.maxQuantity2[index]) : ""; double angle = itemAngle(nPlaces, innerIndex); bool isOnTop = isSlotExpanded && (90.0 < angle && angle < 270.0); vector2 pos = mCenter; pos.y += mScreen.getWheelRadius() * (isOnTop ? -1 : 1); mText.drawBox(ammo1, description, ammo2, pos, !isOnTop, mBaseColor, mAlpha); }
7.8.15. Draw pointer
private void drawPointer(double angle, double radius) { vector2 pos = (sin(angle), -cos(angle)) * radius + mCenter; vector2 size = TexMan.getScaledSize(mTextureCache.pointer); size *= mScaleFactor; Screen.drawTexture( mTextureCache.pointer , NO_ANIMATION , pos.x , pos.y , DTA_CenterOffset , true , DTA_Alpha , mAlpha , DTA_DestWidth , int(size.x) , DTA_DestHeight , int(size.y) ); }
7.8.16. Constants
const NO_ANIMATION = 0; // == false const MARGIN = 4; const UNDEFINED_INDEX = -1; const MAX_N_PIPS = 10; const PIPS_GAP = 1.2; const PIPS_STEP = 1.5; const FILLED_QUANTITY_COLOR = 0x22DD22;
7.8.17. Variables
private double mAlpha; private color mBaseColor; private vector2 mCenter; private gb_Screen mScreen; private gb_Options mOptions; private gb_FontSelector mFontSelector; private gb_MultiWheelMode mMultiWheelMode; private gb_Text mText; private bool mIsRotating; // cache private gb_TextureCache mTextureCache; private double mScaleFactor;
8. Test
8.1. Test actions
Alias gbt2 "netevent gbt2 3 4 5" alias "gbt 3" "netevent gbt3" alias gbt5 "event gbt5"
AddOptionMenu OptionsMenu { Submenu "Gearbox test", gbt_Options } AddOptionMenu OptionsMenuSimple { Submenu "Gearbox test", gbt_Options } OptionMenu gbt_Options { Control "Action 1", "netevent gbt1 2 3 4" Control "Action 2", gbt2 Control "Action 3", "gbt 3" Control "Action 4", "event gbt4 5 6 7" Control "Action 5", gbt5 }
8.2. Run scripts
Basic check: the mod can be loaded without errors.
wait 2; map map01; wait 7; quit