diff --git a/MHWI_LLBG/AmmoWhite.ico b/MHWI_LLBG/AmmoWhite.ico new file mode 100644 index 0000000..e4613d9 Binary files /dev/null and b/MHWI_LLBG/AmmoWhite.ico differ diff --git a/MHWI_LLBG/AutoHotkey.exe b/MHWI_LLBG/AutoHotkey.exe new file mode 100644 index 0000000..5547c89 Binary files /dev/null and b/MHWI_LLBG/AutoHotkey.exe differ diff --git a/MHWI_LLBG/MHWI_LLBG.ahk b/MHWI_LLBG/MHWI_LLBG.ahk new file mode 100644 index 0000000..fb6cf5f --- /dev/null +++ b/MHWI_LLBG/MHWI_LLBG.ahk @@ -0,0 +1,485 @@ +īģŋ/* + 🏹 Ledge Light Bow Gun (LLBG) AHK Script - Ledge Bounce Edition 🏹 + + Author: Luciano Cirino (TSC) + Date: November 2023 + Version: v1.00 + Language: AutoHotkey_H v2 (x64w_MT) https://hotkeyit.github.io/v2/ + + 📖 Notes: + - Compatible Games: 'Monster Hunter World: Iceborne' & 'Iceborne Community Edition' (ICE). + - Game Version: v15.20 + - Control: The script is built to be maneuvered using a PS4 controller. + - Required Mods: "Better Input Detection" mod by AsteriskAmpersand https://www.nexusmods.com/monsterhunterworld/mods/4333 + - Framerate requirements: Ledge bouncing only works at >240 FPS. + - Other requirements: A 'Recoil Suppressor' mod must not be equipped. + + 🌟 Special Thanks: + - Moonbunnie: For his invaluable contribution of memory pointers & research vital to the script's conjuration. + He was also the architect behind the original LLBG scripts, which profoundly influenced this project's design. + +*/ + +;;==============================[Load Libraries]=============================== +SetTitleMatchMode 2 ; Avoids the need to specify the full path of files. +#Include lib/Initialization.ahk +#Include lib/MemoryLibrary.ahk +#Include lib/Functions.ahk +#Include config/LoadSettings.ahk +#Include config/bMaps.ahk +#Include ammo/AmmoControl.ahk +#Include ammo/AmmoScript.ahk + +;;=============================[Set Tray Icon]================================ +TraySetIcon("AmmoWhite.ico") + +;;=============================[On Exit Cleanup]=============================== +Exit(){ + try + ClearKeyStates(keysToClear) + try + CleanExit() + try{ + OffSound() + Sleep(1000) + } + try + AHKPanic(1,0,0,1) + catch + ExitApp +} +OnExit.Bind(Exit) + +;;=============================[Main Function]================================= +/* MHW Memory Reads Functions: + GetAmmoType() ;Returns your Current equipped Ammo type + GetAmmoCount(type) ;Returns the Ammo mag count of a given ammo type (ex:Spread3 8mag, 60clip) + GetAmmoCount2(type) ;Returns the Ammo clip count of a given ammo type + GetActionID() ;Returns your character's Action State ID +*/ +global inLOOP + +LLBG() { + + global inLOOP := FALSE + LastState := -1 + currentStateStartTime := 0 + WindowInFocus() + AutoAmmoSync() + + init() { + if inLOOP{ + ;Tooltip(LastState) + ClearKeyStates(keysToClear) + resetAmmoScript() + global inLOOP:= FALSE + resetFPS() + } + } + + resetFPS() { + if FPS_DIP_RELOAD + SetFPS(300) + } + + ; Define timeouts for different states + StateDurationLimit(state) { + return (state = 13) ? 251 ;Air Melee + : (state = 18) ? 151 ;Rolling [U] + : (state = 23) ? 151 ;Falling [U] + : (state = 25) ? 201 ;Land {Nt} [U] + : (state = 27) ? 201 ;Land {Mv} [U] + : (state = 29) ? 251 ;Land from Air Melee + : (state = 30) ? 501 ;Ledge Step-up + : (state = 72) ? 451 ;'High' Recoil Shot + : (state = 73) ? 251 ;'Very High' Recoil Shot + : (state = 74) ? 551 ;Rapid Fire Shot + : (state = 81) ? 251 ;Air Reload + : (state = 83) ? 501 ;Landing Reload + : 51 ; default value + } + + BounceShotTiming(ammoName){ + type := AMMO_BOUNCE_MAP[ammoName] + switch type { + case 1: ; 'High' Recoil - Normal Shot + return 67 + case 2: ; 'Very High' Recoil - Normal Shot + return 17 + case 3: ; 'High' Recoil - Rapid-Fire Shot + return 17 + default: ; Everythting else + return 0 + } + } + + ; Waits for MHW window to be in focus + WindowInFocus(){ + if !CheckWindow(){ + init() + While (!CheckWindow()) + PreciseSleep(701) + } + } + + Loop{ ;Main Loop + + WindowInFocus() + + ;Memory Read character state + currentState := GetActionID() + + ;Capture the change in state + if (currentState != LastState){ + stateChanged := TRUE + currentStateStartTime := QPC() + } + else + stateChanged := FALSE + + ;Re-init if any state lasts longer than Xms && not in Loop or LBG Avg/Fast Reloading (Anim2ID=107) + if ((((QPC() - currentStateStartTime) > StateDurationLimit(currentState)) && inLOOP) || GetAnim2ID()=107) + init() + + if stateChanged { + ; Upress keys after certain states + if (LastState = 23) + SendKey([keyForward, KeyReload], "Up") + + ; If equipment menu closed, reload script + if (LastState = 585) + ScriptReload("") + + } + + /* + Legend: + [S] Sheathed + [U] Unsheathed + {Nt} Neutral + {Mv} Moving + {St} Stopping + + LBG Action IDs: + 0 - {Nt} + 1 - {Mv} [U] + 2 - {St} [U] + 4 - {Mv} [S] + 5 - {St} [S] + 5 - Unsheathing {Nt} + 6 - Sheathing {Nt} + 7 - Unsheathing {St} + 9 - Sheathing {St} + 13 - Unsheathed Walk Start (+PrimaryID 117) || Air Melee (+PrimaryID 150) + 18 - Rolling [U] + 23 - Falling [U] + 24 - Falling after jump [U] + 25 - Land {Nt} [U] + 27 - Land {Mv} [U] + 29 - Land from Air Melee + 30 - Ledge Step-up + 31 - Ledge Climb-up + 65 - ADS Transition On {Nt} + 66 - ADS transition Off {Nt} + 67 - ADS {Nt} + 69 - ADS {Mv} (when walking there are no transition anims) + 71 - ADS {St} + 72 - 'High' Recoil Normal Shot + 73 - 'Very High' Recoil Normal Shot + 74 - 'High' Rapid Fire Shot + 75 - 'Very High' Recoil Rapid Fire Shot + 78 - 'Slow' Reload + 81 - Air Reload + 83 - Landing Reload + 97 - RF Air Shot + 99 - Gun Melee + 313 - Quest Start Walk + 314 - Bird Quest Entrance + 319 - Drunkbird Quest Entrance + + Animation ID 2: + 125 - 'Low' & 'Average' Recoil Normal Shots + + */ + + ;;Selection depending on current state + switch currentState { + ;____________________________________ + case 0: ;;{Nt} + + ; 'High Recoil' Regular Ammo First Bounce Shot (requires a 34ms delay on first shot) + if (stateChanged && inLOOP && LastState=30 && KeyActive(keyForward)){ + QPCSleep(17) + SendKey(keyForward, "Up") + QPCSleep(17) + SendKey([keyADS, keyFire]) + QPCSleep(17) + SendKey(keyFire, "Up") + AmmoCycle() + } + + ;____________________________________ + case 2: ;;{St} [U] + + ; Bounce shot + if (stateChanged && inLOOP){ + + if (!reload || isRapidFire){ + SendKey(keyADS,"Up") + ammoName:=GetAmmoName(GetAmmoType()) + + ; Retrieve the base time for a bounce shot based on the ammo type + bounceShotTime := BounceShotTiming(ammoName) + + ; Adjust the bounce shot time if reloading and it's a rapid-fire + bounceShotTime += (reload && isRapidFire) ? 17 : 0 + + QPCSleep(bounceShotTime) + + SendKey([keyFire, keyADS]) + QPCSleep(17) + SendKey(keyFire, "Up") + } + + ;AmmoCycle here only if type 2 ammo + if AMMO_BOUNCE_MAP[ammoName]=2 + AmmoCycle() + } + + ;____________________________________ + case 13: ;;Air Melee + + ;; If Air Melee while Primary ammo is selected, start LLBG + if (stateChanged && (GetAmmoType()=GetAmmoID(PRIMARY_AMMO)) && GetAnimID()=150){ + inLOOP := TRUE + AmmoCycle() + } + + ;____________________________________ + case 18: ;;Rolling [U] + + ;; Do nothing + + ;____________________________________ + case 23: ;;Falling [U] + + if stateChanged + resetFPS() + + timeInState := QPC() - currentStateStartTime + + if ((timeInState > 17)){ + if (GetAmmoType()=GetAmmoID(PRIMARY_AMMO)){ + SendKey([keyForward, keyReload]) + AMMO_SCRIPT_Q := 0 + }else if inLOOP + AmmoSwap(PRIMARY_AMMO) + } + + ;____________________________________ + case 25: ;;Land {Nt} [U] + + if (stateChanged && inLOOP && reload && !isRapidFire) + SendKey([keyFire, keyADS]) + + ;____________________________________ + case 27: ;;Land {Mv} [U] + + ;; Do nothing + + ;____________________________________ + case 29: ;;Land from Air Melee + + if (stateChanged && inLOOP) + TimedPulse([keyRoll, keyForward], 51, onDelay:=101) + + ;____________________________________ + case 30: ;;Ledge Step-up + + if (stateChanged) + canStartLoop := true + + ammoName := GetAmmoName(GetAmmoType()) + + if (ammoName=PRIMARY_AMMO && GetCurrentAmmoCount() > 0 && canStartLoop){ + inLOOP := TRUE + canStartLoop := false + switch AMMO_BOUNCE_MAP[ammoName] { + case 1: ; 'High' Recoil - Normal Shot + SendKey(keyForward) + case 2: ; 'Very High' Recoil - Normal Shot + SendKey([keyADS, keyFire]) + case 3: ; 'High' Recoil - Rapid-Fire Shot (only works at 300fps) + SendKey([keyADS, keyFire]) + } + } + + ;____________________________________ + case 65, 67: ;;ADS Transition On {Nt} (65) + ;;ADS {Nt} (67) + + ;; Do nothing + + ;____________________________________ + case 72, 73, 74, 75: ;;'High' Recoil Normal Shot (72) + ;;'Very High' Recoil Normal Shot (73) + ;;'High' Recoil Rapid Fire Shot (74) + ;;'Very High' Recoil Rapid Fire Shot (75) + + if (stateChanged && inLOOP){ + + ammoType := GetAmmoType() + + SendKey(keyFire, "Up") + + ;Wait long enough for Ammo counter to update + QPCSleep(101) + + ; Send info to AmmoScript function to decide what should be done next + nextAction := AmmoScript(GetAmmoName(ammoType), frameDip:=FPS_DIP_RELOAD) + swapToAmmo := nextAction.swapToAmmo + reload := nextAction.reload + isRapidFire := (AMMO_BOUNCE_MAP[swapToAmmo] = 3) ? true : false + + ; Ammo Cycling done here on type 1 & type 3 Ammos + cycleIfSame := (currentState = 72 || currentState = 74 || currentState = 75) ? true : false + + AmmoSwap(swapToAmmo,swapOnEmpty:=true,cycleIfSame:=cycleIfSame) + + ; If fps_dip_reload is enabled, wait some time before dropping framerate to 60 (shot type dependant) + if (FPS_DIP_RELOAD && swapToAmmo=PRIMARY_AMMO && reload){ + switch currentState { + case 72: + QPCSleep(117) + case 73: + QPCSleep(34) + case 74: + QPCSleep(351) + } + SetFPS(60) + } + else if DEEP_BOUNCE + SendKey(keyADS,"Up") + + } + + ;____________________________________ + case 81: ;;Air Reload + + if (stateChanged && GetAmmoType()=GetAmmoID(PRIMARY_AMMO)){ + inLOOP := TRUE + TimedPulse(keyCraft, 17) + } + + ;____________________________________ + case 83: ;;Landing Reload + + if (inLOOP && stateChanged){ + ammoType_was := GetAmmoType() + ammoCount_was := GetAmmoCount(ammoType_was) + SendKey(keyForward) + SendKey(keyADS, "Up") + AmmoCycle() + } + + if (inLOOP && GetAmmoCount(ammoType_was)!=ammoCount_was){ + AmmoCycle() + TimedPulse([keyRoll, keyForward, keyItemLeft, keyItemRight], 17) + } + + ;____________________________________ + case 313: ;;Quest Start Walk + + if stateChanged{ + ReScanPtrs() + AutoAmmoSync() + } + + ;____________________________________ + case 314: ;;Quest Start Bird Drop + + if stateChanged + ReScanPtrs() + + ;____________________________________ + case 319: ;;Quest Start Drunk Bird (Animation starts at 318 but on 319 you can scroll items) + + if stateChanged{ + ReScanPtrs() + AutoAmmoSync() + } + + ;____________________________________ + default: + + if stateChanged + init() + + } + + LastState := currentState + + ; Limit Main loop's speed + if inLOOP + QPCSleep(2) ;If loop is not running at near max speed, states are missed + else + PreciseSleep(34) + } + +} + +;;=============================[Assign Hotkeys]================================ +; Set up the dynamic hotkeys +if (keyScriptReload != "") + Hotkey keyScriptReload, ScriptReload +if (keyScriptExit != "") + Hotkey keyScriptExit, ScriptExit +if (AMMO_SYNC_HOTKEY != "") + Hotkey AMMO_SYNC_HOTKEY, AmmoSyncHotkey +if (PRIMARY_HOTKEY != "") + Hotkey PRIMARY_HOTKEY, Primary_Q +hotkeyMap := Map() +; Generate hotkeys based on the Ammo Script table +for index, value in AMMO_SCRIPT.Array { + if (value.Hotkey != "-" && !hotkeyMap.Has(value.Hotkey)){ + Hotkey value.Hotkey, Script_Q + hotkeyMap[value.Hotkey] := true + } +} + +; Use PS4 Left Trigger to stop LLBG +Joy7::{ + ClearKeyStates(keysToClear) + global inLOOP := False +} + +ScriptReload(hotkeyName){ + CleanExit() + Reload +} + +ScriptExit(hotkeyName){ + Exit() +} + +AmmoSyncHotkey(hotkeyName){ + AmmoSync() +} + +Primary_Q(hotkeyName) { + if ENABLE_AMMO_CONTROL{ + AMMO_SCRIPT.Queue := hotkeyName + if !inLOOP + AmmoSwap(PRIMARY_AMMO) + } +} + +Script_Q(hotkeyName){ + global AMMO_SCRIPT + if ENABLE_AMMO_CONTROL && inLOOP + AMMO_SCRIPT.Queue := hotkeyName +} + +OnSound() +LLBG() +Return \ No newline at end of file diff --git a/MHWI_LLBG/ammo/AmmoControl.ahk b/MHWI_LLBG/ammo/AmmoControl.ahk new file mode 100644 index 0000000..7580472 --- /dev/null +++ b/MHWI_LLBG/ammo/AmmoControl.ahk @@ -0,0 +1,209 @@ +īģŋ/* + đŸ”Ģ Ammo Control Script + + 📡 Description: + This script functions on a dedicated thread for real-time ammo management. + Its job is to manage and execute fully asynchronous ammo wheel related commands. + + 🧩 Features: + ├─ AmmoSync(): + │ └─ Catalogues available ammo types into an array. + │ └─ Saves the array to an INI file for quick loading on startup. + │ + ├─ AmmoSwap(targetAmmoName, swapOnEmpty:=true, cycleIfSame:=false): + │ └─ Efficiently navigates to a target ammo type using the pre-synced ammo array. + │ └─ Parameters: + │ ├─ targetAmmoName: The ammo type to switch to. + │ ├─ swapOnEmpty: Allows swapping even if the current ammo is depleted. + │ └─ cycleIfSame: Initiates AmmoCycle if the target ammo is already selected. + │ + └─ AmmoCycle(): + └─ Cycles the current ammo selection in order to open or keep the ammo wheel active. + +*/ + +AmmoControl := " +( +Persistent +#MaxThreads 1 +#NoTrayIcon +#SingleInstance Force + +;;==============================[Load Libraries]=============================== +SetTitleMatchMode 2 ; Avoids the need to specify the full path of files. +#Include lib/Initialization.ahk +#Include lib/MemoryLibrary.ahk +#Include lib/Functions.ahk +#Include config/LoadSettings.ahk + +ammoListPath := "ammo/ammo_list.ini" +AmmoArray := ini2Array(ammoListPath, "AmmoList") ; AmmoArray = [ ammo_type_1, ammo_type_2, ...] + +; Waits for ammo change, returns false if timedout. +WaitForAmmoChange(AmmoType, Timeout) { + CurrentTime := QPC() + + ; Wait until ammo changes + While (AmmoType = GetAmmoType()) { + ElapsedTime := QPC() - CurrentTime + if (ElapsedTime > Timeout) + return false + } + return true +} + +; Waits for an ammo selection, returns false if timedout. +WaitForAmmoSelect(AmmoType, Timeout) { + CurrentTime := QPC() + + ; Wait until ammo is selected + While (AmmoType != GetAmmoType()) { + ElapsedTime := QPC() - CurrentTime + if (ElapsedTime > Timeout) + return false + } + return true +} + +AmmoSync(){ + + Critical + + global AmmoArray := [] + + AmmoArray.Push(GetAmmoType()) + + ;Begin opening the ammo menu(84ms delay if menu not open, assume not open) + TimedPulse([keyAmmoDown], 17, OffDelay:=17) + + if !WaitForAmmoChange(AmmoArray[AmmoArray.Length], 51) ;51ms lower limit + return + + ;Scroll through ammos until you loop + while (AmmoArray[1]!=GetAmmoType()){ + + AmmoArray.Push(GetAmmoType()) + + TimedPulse([keyAmmoDown], 17, OffDelay:=17) + + if !WaitForAmmoChange(AmmoArray[AmmoArray.Length], 334) ;334ms lower limit + return + } + SendKey(keyAmmoDown, "Up") + + ; Generate a new the ammo_list.ini file + Array2Ini(ammoListPath, "AmmoList", AmmoArray) + +} + +AmmoSwap(targetAmmoName,swapOnEmpty:=true,cycleIfSame:=false){ + + Critical + + ; Get the target ammo ID + targetAmmoType := GetAmmoID(targetAmmoName) + + ; Optional argument to prevent swapping on empty clips + if (!swapOnEmpty && GetAmmoCount(targetAmmoType) <= 0) + return + + ; Ensure the desired ammo type exists in pouch. + if (GetAmmoCount2(targetAmmoType) <= 0) + return + + currentAmmoType := GetAmmoType() + + ; If already on the target ammo, no need to swap. + if (currentAmmoType = targetAmmoType){ + if cycleIfSame + AmmoCycle() + return + } + + ; Locate the positions of current and target ammo in the array. + currentPosition := getArrayValueIndex(AmmoArray, currentAmmoType) + targetPosition := getArrayValueIndex(AmmoArray, targetAmmoType) + + if (currentPosition = "" || targetPosition = "") + return + + ; Decide the quickest scroll direction to get to the desired ammo type. + if (currentPosition >= targetPosition) + distUp := currentPosition - targetPosition + else + distUp := AmmoArray.Length - targetPosition + currentPosition + + distDown := AmmoArray.Length - distUp + scrollKey := (distUp < distDown) ? keyAmmoUp : keyAmmoDown + + ; Swap to the desired ammo type. + while (GetAmmoType() != targetAmmoType) { + + if !CheckWindow() + return + + currentAmmoType := GetAmmoType() + TimedPulse([scrollKey], 17, OffDelay := 17) + + if !WaitForAmmoChange(currentAmmoType, 367) + return + } +} + +AmmoCycle(mode:="Down"){ + + Critical + + ;CycleTime:= QPC() ; Debugging + + currentType := GetAmmoType() + + Switch mode{ + case "Up": + key1stDirection := keyAmmoUp + key2ndDirection := keyAmmoDown + default: + key1stDirection := keyAmmoDown + key2ndDirection := keyAmmoUp + } + + ;TimedPulse([key1stDirection, key2ndDirection], 17, OffDelay:=17) + + TimedPulse(key1stDirection, 17, OffDelay:=17) + TimedPulse(key2ndDirection, 17) + + if !WaitForAmmoSelect(currentType, 451) + return + + QPCSleep(17) + + ;Tooltip(QPC()-CycleTime) ; Debugging + +} + +return +)" + +if (ENABLE_AMMO_CONTROL) { + AmmoControlThread := NewThread(AmmoControl) ; Initial thread takes ~16ms to create +} + +AmmoSync() { + if ENABLE_AMMO_CONTROL + AmmoControlThread.ahkPostFunction("AmmoSync") +} + +AutoAmmoSync(){ + if AUTO_AMMO_SYNC + AmmoSync() +} + +AmmoSwap(targetAmmoName, swapOnEmpty:=true, cycleIfSame:=false) { + if ENABLE_AMMO_CONTROL + AmmoControlThread.ahkPostFunction("AmmoSwap", targetAmmoName, swapOnEmpty . "", cycleIfSame . "") +} + +AmmoCycle(mode:="Down") { + if ENABLE_AMMO_CONTROL + AmmoControlThread.ahkPostFunction("AmmoCycle", mode) +} \ No newline at end of file diff --git a/MHWI_LLBG/ammo/AmmoScript.ahk b/MHWI_LLBG/ammo/AmmoScript.ahk new file mode 100644 index 0000000..dade578 --- /dev/null +++ b/MHWI_LLBG/ammo/AmmoScript.ahk @@ -0,0 +1,281 @@ +/* + 🌟 Ammunition Script(s) 🌟 + + 📜 Overview: + Each script in 'AMMO_SCRIPT' orchestrates a pre-configured set of ammunition controls within the game. + These entries form a sophisticated mechanism for in-game ammo management. + + 🎛ī¸ 'AMMO_SCRIPT' Structure: + ├─ Queue: String ➤ Queued hotkey string + ├─ Running: String ➤ The running script, defined by a hotkey string + ├─ Index: ➤ Current ammo sequence position. + ├─ HkIdxMap: Map ➤ { "HotkeyString": { Array: [array of indexes], Current: Int } } + ├─ ShotCount: Int ➤ Tally of shots dispensed. + └─ Array: Array ➤ [{AmmoName, Hotkey, StartCond, ShotLimit, OnFinish}, ...] + ├─ AmmoName: String ➤ e.g., "Spread 2" + ├─ Hotkey: String ➤ Activates ammo, e.g., "Joy11" + ├─ StartCond: String ➤ Ammo start condition, e.g., "Primary Empty" + ├─ ShotLimit: Int ➤ Caps number of shots, -1 for no limit + └─ OnFinish: String ➤ Next action, e.g., "Goto 2" + +*/ +#Include ../lib/MemoryLibrary.ahk + +resetAmmoScript(include_index:=true){ + global AMMO_SCRIPT + + ; Reset the queued script + AMMO_SCRIPT.Queue := "" + + ; Reset the running script + AMMO_SCRIPT.Running := "" + + ; Reset ammo script index + if include_index + AMMO_SCRIPT.Index := 1 + + ; Reset the shot count + AMMO_SCRIPT.ShotCount := 0 +} + +AmmoScript(ammoName, frameDip:=false){ + + /* + 🔁 Ammunition & Reload Directive Function + + 📄 Description: + Orchestrates the calculated ammo swap and reload maneuver in adherence to a predefined ammo script. + This function is invoked after a shot has been discharged and the ammo count has been refreshed. + + 🛠ī¸ Usage: + Invoke post-discharge with the updated ammo count to determine the next ammo swap and reload necessity. + + 📋 Parameters: + ├─ ammoName: String identifier for the type of ammunition used. + ├─ frameDip: Boolean flag indicating if a reload is triggered by a frame rate dip. + + đŸ“Ļ Returns: + An object encapsulating the next ammunition type as a string and the requisite reload action as a boolean. + Example: {swapToAmmo: "Spread 3", reload: true} + */ + + ; Returns the next array index value (wraps at end) + nextIndex(current_index, array_length){ + current_index++ + if current_index > array_length + return 1 + else + return current_index++ + } + + findNextIndex(start_at_index, ammo_script_array, current_ammo_name){ + + ; Returns the index within the ammo script entry that the script is set to proceed to. + ; If not going to an entry in the ammo script array, returns 0. + + ; If already inside script, you can not not include yourself in search + start_index_allowed := !(ammo_script_array[start_at_index].AmmoName = current_ammo_name) + + j := ammo_script_array.Length + + if start_index_allowed + i := start_at_index + + else if (ammo_script_array[start_at_index].OnFinish = "Return") + return 0 ; return to primary + + else{ + i := nextIndex(start_at_index, j) + if i = start_at_index + return 0 ; looped around, no other entries available + } + + Loop j { ; Begin searching for an entry with ammo + + if (GetAmmoCount(GetAmmoID(ammo_script_array[i].ammoName)) > 0) + return i ; entry with ammo found, return index position + else if (ammo_script_array[i].OnFinish = "Return") + return 0 ; return to primary + + i := nextIndex(i, j) + } + return 0 ; no entry with ammo found + } + + returnHkIdxWithAmmo(hotkey){ + ; Returns the ammo script index within the hotkey index map that has ammo. + ; Updates the current hotkey index map position to the found ammo. + ; If none is found, returns 0. + global AMMO_SCRIPT + + hotkey_index_array := AMMO_SCRIPT.HkIdxMap[hotkey].Array + start_at_index := AMMO_SCRIPT.HkIdxMap[hotkey].Current + + j := hotkey_index_array.Length + i := start_at_index + + Loop j { ; Begin searching for an entry with ammo + + if (GetAmmoCount(GetAmmoID(AMMO_SCRIPT.Array[hotkey_index_array[i]].ammoName)) > 0){ + AMMO_SCRIPT.HkIdxMap[hotkey].Current := i + return hotkey_index_array[i] ; entry with ammo found, return index value + } + i := nextIndex(i, j) + } + return 0 ; no entry with ammo found + } + + ; ... + + global AMMO_SCRIPT + + ; Initialize primary ammo conditions & variables + i_am_primary_ammo := (ammoName = PRIMARY_AMMO) + primary_ammo_count := GetAmmoCount(GetAmmoID(PRIMARY_AMMO)) + primary_needs_reload := ((frameDip && primary_ammo_count = 0) || (!frameDip && primary_ammo_count <= 1)) + + ; =============================================================================== + ; Decide how to proceed based on the queued hotkey & the currently running script + ; =============================================================================== + queued_hotkey := AMMO_SCRIPT.Queue + running_hotkey := AMMO_SCRIPT.Running + sameScriptQueued := (queued_hotkey = running_hotkey) + primaryIsQueued := (queued_hotkey = PRIMARY_HOTKEY) || sameScriptQueued + scriptIsQueued := (queued_hotkey != "" && !primaryIsQueued) + scriptIsRunning := (running_hotkey != "") + newScriptQueued := scriptIsQueued && (queued_hotkey != running_hotkey) + + if (primaryIsQueued && i_am_primary_ammo) { + ; Reset the queued hotkey + AMMO_SCRIPT.Queue := "" + + ; Update the running script + AMMO_SCRIPT.Running := "" + + return {swapToAmmo: PRIMARY_AMMO, reload: primary_needs_reload} + } + + ; If no script is queued or running, swap to primary ammo and reload if needed. + else if (!scriptIsQueued && !scriptIsRunning) + return {swapToAmmo: PRIMARY_AMMO, reload: primary_needs_reload} + + else if newScriptQueued{ + ; Reset the queued hotkey + AMMO_SCRIPT.Queue := "" + + ; Find the next index in the HkIdxMap with ammo + hk_index := returnHkIdxWithAmmo(queued_hotkey) + + if (hk_index != 0){ ; If index with ammo found, update the running script + AMMO_SCRIPT.Running := queued_hotkey + AMMO_SCRIPT.Index := hk_index + AMMO_SCRIPT.ShotCount := 0 + } else ; return to primary + return {swapToAmmo: PRIMARY_AMMO, reload: primary_needs_reload} + + } + + ; ======================================================= + ; Unpack & initialize ammo script variables & conditions + ; ======================================================= + array := AMMO_SCRIPT.Array + index := AMMO_SCRIPT.Index + shot_count := AMMO_SCRIPT.ShotCount + start_condition := array[index].StartCond + script_ammo_name := array[index].AmmoName + script_ammo_limit := array[index].ShotLimit + script_finish_action := array[index].OnFinish + script_ammo_count := GetAmmoCount(GetAmmoID(script_ammo_name)) + i_am_script_ammo := (ammoName = script_ammo_name) + next_index := findNextIndex(index, array, ammoName) + will_swap_into_script := (next_index > 0) + + ; ============================================================ + ; Prevent entering script until the 'Start Conditions' are met + ; ============================================================ + ; If emptying the primary ammo, check wether the ammo we are swapping to only has one shot and is returning to + ; primary when complete. If this is the case send a reload command. + if (i_am_primary_ammo && (start_condition = "Empty") && will_swap_into_script){ + next_next_index := findNextIndex(next_index, array, array[next_index].AmmoName) + next_script_entry_will_exit := (next_next_index = 0) + next_script_ammo_count := GetAmmoCount(GetAmmoID(array[next_index].AmmoName)) + next_script_ammo_limit := array[next_index].ShotLimit + + if (next_script_entry_will_exit && ((next_script_ammo_count = 1) || (next_script_ammo_limit = 1)) && (primary_ammo_count = 0)) + return {swapToAmmo: array[next_index].AmmoName, reload: true} + else if (primary_ammo_count > 0) + return {swapToAmmo: ammoName, reload: false} + } + + ; ================================================== + ; Continue shot counting if am script ammo, else... + ; ================================================== + ; If am script ammo, update shot count + if i_am_script_ammo { + if primaryIsQueued{ + AMMO_SCRIPT.ShotCount := primary_needs_reload ? script_ammo_limit - 1 : script_ammo_limit + AMMO_SCRIPT.Queue := "" + } + else + AMMO_SCRIPT.ShotCount++ + shot_count := AMMO_SCRIPT.ShotCount + } + ; Else if swapping into script + else if will_swap_into_script { + AMMO_SCRIPT.Index := next_index + return {swapToAmmo: array[next_index].AmmoName, reload: false} + + ; Else re-initialize the current ammo script & return to primary + } else { + resetAmmoScript() + return {swapToAmmo: PRIMARY_AMMO, reload: primary_needs_reload} + } + + ; note: Only the current script ammo will make it past this point! + + ; ============================================================= + ; Execute the configured 'Finish Actions' if conditions are met + ; ============================================================= + script_actions_complete := ((shot_count >= script_ammo_limit) || (script_ammo_count = 0)) + + ; If shot count is passed, or target script ammo count is 0 + if script_actions_complete{ + if will_swap_into_script { + AMMO_SCRIPT.ShotCount := 0 + AMMO_SCRIPT.Index := next_index + return {swapToAmmo: array[next_index].AmmoName, reload: false} + } else{ + resetAmmoScript(include_index:=false) + hotkey_idx_map := AMMO_SCRIPT.HkIdxMap[running_hotkey] + AMMO_SCRIPT.HkIdxMap[running_hotkey].Current := nextIndex(hotkey_idx_map.Current, hotkey_idx_map.Array.Length) + return {swapToAmmo: PRIMARY_AMMO, reload: primary_needs_reload} + } + } + + ; ============================================================================================== + ; Check wether a reload will be needed on the upcoming shot (reloads must requested in advance) + ; ============================================================================================== + one_script_shot_left := (((shot_count = (script_ammo_limit - 1)) || (script_ammo_count = 1)) && (script_ammo_count != 0)) && !frameDip + + ; If primary needs a reload, and am in script with one shot left, and am moving to another + ; entry in script that only has 1 shot left and that entry will return to primary, send a reload command. + if (primary_needs_reload && will_swap_into_script && one_script_shot_left) { + + next_next_index := findNextIndex(next_index, array, array[next_index].AmmoName) + next_script_entry_will_exit := (next_next_index = 0) + next_script_ammo_count := GetAmmoCount(GetAmmoID(array[next_index].AmmoName)) + next_script_ammo_limit := array[next_index].ShotLimit + + if next_script_entry_will_exit && ((next_script_ammo_count = 1) || (next_script_ammo_limit = 1)) + return {swapToAmmo: ammoName, reload: true} + + ; If primary needs a reload and will be swapping to primary & one shot to go, send a reload command + } else if (primary_needs_reload && !will_swap_into_script && one_script_shot_left){ + return {swapToAmmo: ammoName, reload: true} + } + + ; ... + + ; else continue firing + return {swapToAmmo: ammoName, reload: false} +} \ No newline at end of file diff --git a/MHWI_LLBG/config/LoadSettings.ahk b/MHWI_LLBG/config/LoadSettings.ahk new file mode 100644 index 0000000..93d7305 --- /dev/null +++ b/MHWI_LLBG/config/LoadSettings.ahk @@ -0,0 +1,85 @@ +īģŋ#Include ../lib/_JXON.ahk + +LoadSettingsFromFile(filePath) { + ; Check if the file exists + if !FileExist(filePath) { + MsgBox("The settings file does not exist.") + ExitApp + } + + ; Read settings.json + settings_json := FileRead(filePath) + + ; Load JSON data into an AHK object + settings := Jxon_Load(&settings_json) + + ;;================================[General]================================= + global LBG_NAME := settings["LBG Name"] + global PRIMARY_AMMO := settings["Primary Ammo"] + global DEEP_BOUNCE := settings["Deeper Bounces"] + global FPS_DIP_RELOAD := settings["FPS Dip Reloads"] + + ;;=============================[Script Settings]============================ + global keyScriptReload := settings["keyScriptReload"] + global keyScriptExit := settings["keyScriptExit"] + + ;;===============================[KbMControls]=============================== + global keyForward := settings["keyForward"] + global keyBack := settings["keyBack"] + global keyReload := settings["keyReload"] + global keyRoll := settings["keyRoll"] + global keyAmmoUp := settings["keyAmmoUp"] + global keyAmmoDown := settings["keyAmmoDown"] + global keyItemLeft := settings["keyItemLeft"] + global keyItemRight := settings["keyItemRight"] + global keyFire := settings["keyFire"] + global keyADS := settings["keyADS"] + global keyMine := settings["keyMine"] + global keyCraft := settings["keyCraft"] + global keysToClear := [keyForward, keyReload, keyRoll, keyAmmoUp, + keyAmmoDown, keyFire, keyADS, keyMine] + + ;;=============================[Ammo Scripting]============================= + global ENABLE_AMMO_CONTROL := settings["Ammo Script Enabled"] + global PRIMARY_HOTKEY := settings["Primary Hotkey"] + global AMMO_SYNC_HOTKEY := settings["Ammo Sync Hotkey"] + global AUTO_AMMO_SYNC := settings["Auto Ammo Sync"] + + global AMMO_SCRIPT := { + Queue: "", + Running: "", + Index: 1, + HkIdxMap: Map(), + ShotCount: 0, + Array: [], + } + + ; Loop 5 times for the ammo script settings + Loop 5 { + n := A_Index + AMMO_SCRIPT.Array.Push({ + AmmoName: settings[A_Index " Ammo Name"], + Hotkey: settings[A_Index " Queue Hotkey"], + StartCond: settings[A_Index " Start Condition"], + ShotLimit: settings[A_Index " Shot Limit"] = "-" ? 99 : Number(settings[A_Index " Shot Limit"]), + OnFinish: settings[A_Index " Finish Action"] + }) + } + + HotkeyIdxMaps(){ + global AMMO_SCRIPT + + for index, ammo in AMMO_SCRIPT.Array { + hotkey := ammo.Hotkey + if (hotkey = "-") + continue + if (!AMMO_SCRIPT.HkIdxMap.Has(hotkey)) { + AMMO_SCRIPT.HkIdxMap[hotkey] := {Array: [], Current: 1} + } + AMMO_SCRIPT.HkIdxMap[hotkey].Array.Push(index) + } + } + HotkeyIdxMaps() + +} +LoadSettingsFromFile("config\settings.json") \ No newline at end of file diff --git a/MHWI_LLBG/config/bMaps.ahk b/MHWI_LLBG/config/bMaps.ahk new file mode 100644 index 0000000..fc5f70b --- /dev/null +++ b/MHWI_LLBG/config/bMaps.ahk @@ -0,0 +1,188 @@ +īģŋ +/* đŸ—ēī¸ Ammunition Bounce Maps + + The below table shows how ammos are categorized to a bounce type (bType). This bType is used by LLBG script for logic & timings. + + _________________________________________________Ammo_Bounce_Behaviors_________________________________________________________________ + | bType | bSpeed | Recoil | Shot Type | 1st Bounce Delay | Bounce Cycle | Notes | + |-------|----------|------------|------------|------------------|--------------|----------------------------------------------------------| + | 0 | None | * | * | - | - | Every shot type that doesn't bounce is set as 0. | + | 1 | Medium | High | Normal | 34ms | ~530ms | - | + | 2 | Fast | Very High | Normal | 0ms | ~380ms | - | + | 3 | Slow | High | Rapid-Fire | 0ms* | ~700ms | *First bounce only works at 300fps | + --------------------------------------------------------------------------------------------------------------------------------------- + Note: Rapid-Fire with Very High recoil "can" bounce but requires repositioning (aka not worth it). Set to type 0. +*/ + +global AMMO_BOUNCE_MAP := GetbMap(LBG_NAME) + +GetbMap(mapName) { + + ; The below maps assume the light bowguns have no recoil reducing mods or skills + + bMaps := Map( + + "default", Map( + "Normal 1", 0, "Normal 2", 0, "Normal 3", 0, + "Pierce 1", 0, "Pierce 2", 0, "Pierce 3", 0, + "Spread 1", 0, "Spread 2", 0, "Spread 3", 0, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 0, "Sticky 2", 0, "Sticky 3", 0, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 0, "Thunder", 0, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 0, + "Recovery 1", 0, "Recovery 2", 0, + "Demon", 0, "Armor", 0, "Tranq", 0 + ), + + "Accursed Fire", Map( + "Normal 1", 0, "Normal 2", 1, "Normal 3", 2, + "Pierce 1", 0, "Pierce 2", 0, "Pierce 3", 2, + "Spread 1", 0, "Spread 2", 0, "Spread 3", 2, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 2, "Sticky 2", 0, "Sticky 3", 2, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 0, "Thunder", 0, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 0, + "Recovery 1", 0, "Recovery 2", 0, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + + "Bazelcore", Map( + "Normal 1", 0, "Normal 2", 0, "Normal 3", 2, + "Pierce 1", 0, "Pierce 2", 0, "Pierce 3", 2, + "Spread 1", 0, "Spread 2", 0, "Spread 3", 0, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 2, "Sticky 2", 2, "Sticky 3", 2, + "Slicing", 0, "Flaming", 1, "Water", 0, + "Freeze", 0, "Thunder", 1, "Dragon Ammo", 2, + "Poison 1", 0, "Poison 2", 1, + "Paralysis 1", 0, "Paralysis 2", 2, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 2, "Exhaust 2", 2, + "Recovery 1", 0, "Recovery 2", 0, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + + "Fatalis Depths", Map( + "Normal 1", 0, "Normal 2", 0, "Normal 3", 0, + "Pierce 1", 0, "Pierce 2", 3, "Pierce 3", 1, + "Spread 1", 1, "Spread 2", 3, "Spread 3", 2, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 1, "Sticky 2", 0, "Sticky 3", 2, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 0, "Thunder", 0, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 0, + "Recovery 1", 0, "Recovery 2", 0, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + + "Royal Surefire Shot", Map( + "Normal 1", 0, "Normal 2", 0, "Normal 3", 2, + "Pierce 1", 0, "Pierce 2", 0, "Pierce 3", 2, + "Spread 1", 0, "Spread 2", 2, "Spread 3", 2, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 0, "Sticky 2", 2, "Sticky 3", 2, + "Slicing", 2, "Flaming", 0, "Water", 1, + "Freeze", 3, "Thunder", 0, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 1, + "Paralysis 1", 1, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 1, + "Exhaust 1", 2, "Exhaust 2", 2, + "Recovery 1", 0, "Recovery 2", 0, + "Demon", 0, "Armor", 1, "Tranq", 1 + ), + + "Safi's Aquashot", Map( + "Normal 1", 0, "Normal 2", 0, "Normal 3", 0, + "Pierce 1", 0, "Pierce 2", 0, "Pierce 3", 1, + "Spread 1", 0, "Spread 2", 0, "Spread 3", 0, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 0, "Sticky 2", 0, "Sticky 3", 2, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 3, "Thunder", 3, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 0, + "Recovery 1", 0, "Recovery 2", 1, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + + "Safi's Boltshot", Map( + "Normal 1", 0, "Normal 2", 1, "Normal 3", 0, + "Pierce 1", 0, "Pierce 2", 1, "Pierce 3", 0, + "Spread 1", 0, "Spread 2", 1, "Spread 3", 0, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 2, "Sticky 2", 0, "Sticky 3", 0, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 3, "Thunder", 3, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 0, + "Recovery 1", 0, "Recovery 2", 1, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + + "Safi's Drakshot", Map( + "Normal 1", 0, "Normal 2", 3, "Normal 3", 0, + "Pierce 1", 0, "Pierce 2", 0, "Pierce 3", 0, + "Spread 1", 0, "Spread 2", 0, "Spread 3", 2, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 0, "Sticky 2", 0, "Sticky 3", 0, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 0, "Thunder", 0, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 0, + "Recovery 1", 0, "Recovery 2", 1, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + + "Safi's Frostshot", Map( + "Normal 1", 0, "Normal 2", 1, "Normal 3", 0, + "Pierce 1", 0, "Pierce 2", 1, "Pierce 3", 0, + "Spread 1", 0, "Spread 2", 1, "Spread 3", 0, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 2, "Sticky 2", 0, "Sticky 3", 0, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 3, "Thunder", 0, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 1, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 1, + "Recovery 1", 0, "Recovery 2", 1, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + + "Ten Thousand Volts", Map( + "Normal 1", 0, "Normal 2", 0, "Normal 3", 2, + "Pierce 1", 0, "Pierce 2", 0, "Pierce 3", 0, + "Spread 1", 0, "Spread 2", 0, "Spread 3", 2, + "Cluster 1", 0, "Cluster 2", 0, "Cluster 3", 0, + "Sticky 1", 0, "Sticky 2", 0, "Sticky 3", 2, + "Slicing", 0, "Flaming", 0, "Water", 0, + "Freeze", 0, "Thunder", 0, "Dragon Ammo", 0, + "Poison 1", 0, "Poison 2", 0, + "Paralysis 1", 0, "Paralysis 2", 0, + "Sleep 1", 0, "Sleep 2", 0, + "Exhaust 1", 0, "Exhaust 2", 0, + "Recovery 1", 0, "Recovery 2", 0, + "Demon", 0, "Armor", 0, "Tranq", 1 + ), + ) + + ; Return the requested map if it exists, otherwise return the default map + return bMaps.Has(mapName) ? bMaps[mapName] : bMaps["default"] +} \ No newline at end of file diff --git a/MHWI_LLBG/config/settings.json b/MHWI_LLBG/config/settings.json new file mode 100644 index 0000000..154ba97 --- /dev/null +++ b/MHWI_LLBG/config/settings.json @@ -0,0 +1,49 @@ +{ + "LBG Name": "-", + "Primary Ammo": "-", + "Deeper Bounces": false, + "FPS Dip Reloads": false, + "keyScriptReload": "F6", + "keyScriptExit": "F10", + "Ammo Script Enabled": false, + "Primary Hotkey": "Joy12", + "Ammo Sync Hotkey": "Joy13", + "Auto Ammo Sync": false, + "keyForward": "w", + "keyBack": "s", + "keyReload": "Xbutton1", + "keyRoll": "space", + "keyAmmoUp": "up", + "keyAmmoDown": "down", + "keyItemLeft": "left", + "keyItemRight": "right", + "keyFire": "Lbutton", + "keyADS": "Rbutton", + "keyMine": "Xbutton2", + "keyCraft": "", + "1 Ammo Name": "-", + "1 Queue Hotkey": "-", + "1 Start Condition": "None", + "1 Shot Limit": "-", + "1 Finish Action": "Return", + "2 Ammo Name": "-", + "2 Queue Hotkey": "-", + "2 Start Condition": "None", + "2 Shot Limit": "-", + "2 Finish Action": "Return", + "3 Ammo Name": "-", + "3 Queue Hotkey": "-", + "3 Start Condition": "None", + "3 Shot Limit": "-", + "3 Finish Action": "Return", + "4 Ammo Name": "-", + "4 Queue Hotkey": "-", + "4 Start Condition": "None", + "4 Shot Limit": "-", + "4 Finish Action": "Return", + "5 Ammo Name": "-", + "5 Queue Hotkey": "-", + "5 Start Condition": "None", + "5 Shot Limit": "-", + "5 Finish Action": "Return" +} \ No newline at end of file diff --git a/MHWI_LLBG/gui/LBG.ico b/MHWI_LLBG/gui/LBG.ico new file mode 100644 index 0000000..c618925 Binary files /dev/null and b/MHWI_LLBG/gui/LBG.ico differ diff --git a/MHWI_LLBG/gui/compileExe.bat b/MHWI_LLBG/gui/compileExe.bat new file mode 100644 index 0000000..f1795d1 --- /dev/null +++ b/MHWI_LLBG/gui/compileExe.bat @@ -0,0 +1,16 @@ +@echo off +REM Run PyInstaller (requires pyinstaller to be installed) +pyinstaller --noconfirm --onefile --noconsole --icon=LBG.ico gui.py + +REM Rename and move the executable up two directories +move /Y ".\dist\gui.exe" "..\..\LLBG.exe" + +REM Remove the build and dist directories +rmdir /S /Q build +rmdir /S /Q dist + +REM Remove the spec file +del /Q gui.spec + +REM Pause the script to check for errors +pause \ No newline at end of file diff --git a/MHWI_LLBG/gui/gui.png b/MHWI_LLBG/gui/gui.png new file mode 100644 index 0000000..b5e85a8 Binary files /dev/null and b/MHWI_LLBG/gui/gui.png differ diff --git a/MHWI_LLBG/gui/gui.py b/MHWI_LLBG/gui/gui.py new file mode 100644 index 0000000..47d0495 --- /dev/null +++ b/MHWI_LLBG/gui/gui.py @@ -0,0 +1,753 @@ +import json +from PyQt5 import QtCore, QtGui, QtWidgets +import pyautogui +import os +import subprocess +from configparser import ConfigParser +QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) + +LBG_NAMES = ["-", "Accursed Fire", "Bazelcore", "Fatalis Depths", "Royal Surefire Shot", "Safi's Aquashot", "Safi's Drakshot", "Ten Thousand Volts"] +AMMO_NAMES = ["-", "Normal 1", "Normal 2", "Normal 3", "Pierce 1", "Pierce 2", "Pierce 3", + "Spread 1", "Spread 2", "Spread 3", "Sticky 1", "Sticky 2", "Sticky 3", "Slicing", + "Flaming", "Water", "Freeze", "Thunder", "Dragon", "Poison 1", "Poison 2", + "Paralysis 1", "Paralysis 2", "Sleep 1", "Sleep 2", "Exhaust 1", "Exhaust 2", + "Recovery 1", "Recovery 2", "Demon", "Armor", "Tranq"] +HOTKEYS_PS4 = ["-"] + [f"Joy{i}" for i in range(1, 15)] +HOTKEYS_PC = [f"F{i}" for i in range(1, 13)] +CRAFT_KEYS = [f"{i}" for i in range(1, 8)] +START_CONDITIONS = ["None", "Empty"] +SHOT_LIMITS = ["-"] + [f"{i}" for i in range(1, 10)] +FINISH_ACTIONS = ["Return", "Next"] + +class Ui_LLBG(object): + + icon_filePath = "MHWI_LLBG\\gui\\LBG.ico" + ps4joy_filePath = "MHWI_LLBG\\gui\\ps4joy.png" + settings_filePath = "MHWI_LLBG\\config\\settings.json" + runLLBG_filePath = "MHWI_LLBG\\run_llbg.vbs" + ammoList_filePath = "MHWI_LLBG\\ammo\\ammo_list.ini" + + ammo_mapping = { + "0": "Normal 1", "1": "Normal 2", "2": "Normal 3", + "3": "Pierce 1", "4": "Pierce 2", "5": "Pierce 3", + "6": "Spread 1", "7": "Spread 2", "8": "Spread 3", + "9": "Cluster 1", "10": "Cluster 2", "11": "Cluster 3", + "13": "Sticky 1", "14": "Sticky 2", "15": "Sticky 3", + "16": "Slicing", "17": "Flaming", "18": "Water", + "19": "Freeze", "20": "Thunder", "21": "Dragon Ammo", + "22": "Poison 1", "23": "Poison 2", "24": "Paralysis 1", + "25": "Paralysis 2", "26": "Sleep 1", "27": "Sleep 2", + "28": "Exhaust 1", "29": "Exhaust 2", "30": "Recovery 1", + "31": "Recovery 2", "32": "Demon", "33": "Armor", "36": "Tranq" + } + + def setupUi(self, LLBG): + self.configure_window(LLBG) + self.create_group_boxes(LLBG) + self.create_button_boxes(LLBG) + self.create_button_boxes_2() + self.create_button_boxes_3() + self.create_labels(LLBG) + self.create_combo_boxes(LLBG) + self.create_check_boxes(LLBG) + self.create_table_widgets(LLBG) + self.finalize_setup(LLBG) + self.load_settings() + + def configure_window(self, window): + window.setObjectName("LLBG") + window.resize(558, 329) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(window.sizePolicy().hasHeightForWidth()) + window.setSizePolicy(sizePolicy) + window.setMinimumSize(QtCore.QSize(558, 329)) + window.setMaximumSize(QtCore.QSize(558, 329)) + window.setWindowFlags(window.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) + + # Create a function within this scope that handles the close event + def closeEvent(event): + self.stop_script() # Assuming stop_script is defined in this scope + QtWidgets.QWidget.closeEvent(window, event) # Call the default QWidget closeEvent + + # Bind the new close event to the window + window.closeEvent = closeEvent + + def fixed_size_policy(self, widget): + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + size_policy.setHorizontalStretch(0) + size_policy.setVerticalStretch(0) + size_policy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth()) + return size_policy + + def create_group_boxes(self, parent): + self.groupBox_ammoScripting = self.create_group_box(parent, QtCore.QRect(10, 128, 361, 196), "Calibri", 11, "groupBox_ammoScripting", True) + self.groupBox_ammoScripting.setChecked(False) + self.groupBox_general = self.create_group_box(parent, QtCore.QRect(10, 3, 201, 121), "Calibri", 11, "groupBox_general") + self.groupBox_kbmControls = self.create_group_box(parent, QtCore.QRect(384, 3, 161, 281), "Calibri", 11, "groupBox_kbmControls") + self.groupBox_scriptControls = self.create_group_box(parent, QtCore.QRect(220, 3, 151, 121), "Calibri", 11, "groupBox_scriptControls") + + def create_group_box(self, parent, geometry, font_name, font_size, object_name, checkable=False): + group_box = QtWidgets.QGroupBox(parent) + group_box.setGeometry(geometry) + font = QtGui.QFont(font_name, font_size) + group_box.setFont(font) + group_box.setCheckable(checkable) + group_box.setObjectName(object_name) + group_box.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) + return group_box + + def create_button_boxes(self, parent): + self.buttonBoxes = QtWidgets.QDialogButtonBox(parent) + self.buttonBoxes.setGeometry(QtCore.QRect(378, 285, 171, 41)) + self.buttonBoxes.setOrientation(QtCore.Qt.Horizontal) + self.buttonBoxes.setCenterButtons(False) + self.buttonBoxes.setObjectName("buttonBoxes") + + # Create custom buttons + self.restoreDefaultsButton = QtWidgets.QPushButton("â™ģī¸\nReset") + self.saveButton = QtWidgets.QPushButton("💾\nSave") + self.runButton = QtWidgets.QPushButton("â–ļī¸\nRun") + self.stopButton = QtWidgets.QPushButton("🛑\nStop") + + # Add custom buttons to the button box + self.buttonBoxes.addButton(self.restoreDefaultsButton, QtWidgets.QDialogButtonBox.ActionRole) + self.buttonBoxes.addButton(self.saveButton, QtWidgets.QDialogButtonBox.ActionRole) + self.buttonBoxes.addButton(self.runButton, QtWidgets.QDialogButtonBox.ActionRole) + self.buttonBoxes.addButton(self.stopButton, QtWidgets.QDialogButtonBox.ActionRole) + + # Connect the buttons to their respective methods + self.restoreDefaultsButton.clicked.connect(self.restore_defaults) + self.saveButton.clicked.connect(self.save_settings) + self.runButton.clicked.connect(self.run_script) + self.stopButton.clicked.connect(self.stop_script) + + def create_button_boxes_2(self): + self.ps4JoyMapButton = QtWidgets.QPushButton("🎮 PS4 Joy Map", self.groupBox_scriptControls) + self.ps4JoyMapButton.setGeometry(QtCore.QRect(32, 95, 88, 20)) + + # Set the font for the button + font = QtGui.QFont("Calibri", 9, QtGui.QFont.Normal) + self.ps4JoyMapButton.setFont(font) + + # Set padding to push text to the bottom + self.ps4JoyMapButton.setStyleSheet("QPushButton { padding-bottom: 2px; }") + + self.ps4JoyMapButton.clicked.connect(self.open_ps4_joy_map) + + def create_button_boxes_3(self): + self.newButton = QtWidgets.QPushButton("đŸ’ŧ", self.groupBox_ammoScripting) + self.newButton.setGeometry(QtCore.QRect(328, 39, 22, 22)) + + # Set the font for the button + font = QtGui.QFont("Calibri", 9, QtGui.QFont.Normal) + self.newButton.setFont(font) + + # Set padding to push text to the bottom + self.newButton.setStyleSheet("QPushButton { padding-bottom: 2px; }") + + # Set a tooltip for the button + self.newButton.setToolTip("View synchronized ammo list") + + # Connect the button to its respective method + self.newButton.clicked.connect(self.list_ammo_contents) + + def create_dialog_button_box(self, parent, geometry, object_name, standard_buttons): + button_box = QtWidgets.QDialogButtonBox(parent) + button_box.setGeometry(geometry) + button_box.setOrientation(QtCore.Qt.Horizontal) + button_box.setStandardButtons(standard_buttons) + button_box.setObjectName(object_name) + return button_box + + def create_labels(self, parent): + self.label_primaryHotkey = self.create_label(self.groupBox_ammoScripting, QtCore.QRect(10, 25, 121, 20), "Calibri", 9, "label_primaryHotkey") + self.label_AmmoSyncHotkey = self.create_label(self.groupBox_ammoScripting, QtCore.QRect(195, 14, 101, 20), "Calibri", 9, "label_AmmoSyncHotkey") + self.label_primaryHotkeyNote = self.create_label(self.groupBox_ammoScripting, QtCore.QRect(17, 38, 101, 20), "Calibri", 7, "label_primaryHotkeyNote") + self.label_lbgName = self.create_label(self.groupBox_general, QtCore.QRect(10, 22, 71, 20), "Calibri", 10, "label_lbgName") + self.label_primaryAmmo = self.create_label(self.groupBox_general, QtCore.QRect(10, 51, 101, 20), "Calibri", 10, "label_primaryAmmo") + #self.label_kbmControlsNote = self.create_label(self.groupBox_kbmControls, QtCore.QRect(4, 13, 161, 20), "Calibri", 7, "label_kbmControlsNote") + #self.label_applyChangesNote = self.create_label(parent, QtCore.QRect(379, 310, 181, 20), "Calibri", 7, "label_applyChangesNote") + + def create_label(self, parent, geometry, font_name, font_size, object_name): + label = QtWidgets.QLabel(parent) + label.setGeometry(geometry) + font = QtGui.QFont(font_name, font_size) + font.setBold(False) + label.setFont(font) + label.setObjectName(object_name) + return label + + def create_combo_boxes(self, parent): + self.comboBox_primaryHotkey = self.create_combo_box(self.groupBox_ammoScripting, QtCore.QRect(130, 25, 55, 22), "comboBox_primaryHotkey") + self.comboBox_AmmoSyncHotkey = self.create_combo_box(self.groupBox_ammoScripting, QtCore.QRect(295, 14, 55, 22), "comboBox_AmmoSyncHotkey") + self.comboBox_lbgName = self.create_combo_box(self.groupBox_general, QtCore.QRect(70, 22, 121, 22), "comboBox_lbgName") + self.comboBox_primaryAmmo = self.create_combo_box(self.groupBox_general, QtCore.QRect(95, 51, 96, 22), "comboBox_primaryAmmo") + + self.comboBox_primaryHotkey.addItems(HOTKEYS_PS4) + self.comboBox_AmmoSyncHotkey.addItems(HOTKEYS_PS4 + HOTKEYS_PC) + self.comboBox_lbgName.addItems(LBG_NAMES) + self.comboBox_primaryAmmo.addItems(AMMO_NAMES) + + def create_combo_box(self, parent, geometry, object_name): + combo_box = QtWidgets.QComboBox(parent) + combo_box.setGeometry(geometry) + combo_box.setObjectName(object_name) + font = QtGui.QFont("Calibri", 9, QtGui.QFont.Normal) + combo_box.setFont(font) + return combo_box + + def create_check_boxes(self, parent): + self.checkBox_AmmoSync = self.create_check_box( + parent=self.groupBox_ammoScripting, + geometry=QtCore.QRect(195, 38, 132, 21), + font_name="Calibri", + font_size=9, + object_name="checkBox_AmmoSync" + ) + self.checkBox_deepBounces = self.create_check_box( + parent=self.groupBox_general, + geometry=QtCore.QRect(52, 75, 131, 21), + font_name="Calibri", + font_size=10, + object_name="checkBox_deepBounces" + ) + self.checkBox_fpsDipReload = self.create_check_box( + parent=self.groupBox_general, + geometry=QtCore.QRect(52, 97, 131, 21), + font_name="Calibri", + font_size=10, + object_name="checkBox_fpsDipReload" + ) + + def create_check_box(self, parent, geometry, font_name, font_size, object_name): + check_box = QtWidgets.QCheckBox(parent) + check_box.setGeometry(geometry) + font = QtGui.QFont() + font.setFamily(font_name) + font.setPointSize(font_size) + check_box.setFont(font) + check_box.setObjectName(object_name) + return check_box + + def create_table_widgets(self, parent): + self.tableWidget_ammoScripting = self.create_table_widget( + parent=self.groupBox_ammoScripting, + geometry=QtCore.QRect(10, 64, 340, 122), + font_name="Bahnschrift Condensed", + font_size=9, + object_name="tableWidget_ammoScripting", + column_count=5, + row_count=5 + ) + self.initialize_table_widget_items(self.tableWidget_ammoScripting) + + self.tableWidget_kbmControls = self.create_table_widget( + parent=self.groupBox_kbmControls, + geometry=QtCore.QRect(20, 21, 118, 254), + font_name="Calibri", + font_size=8, + object_name="tableWidget_kbmControls", + column_count=1, + row_count=12 + ) + self.initialize_table_widget_items(self.tableWidget_kbmControls) + + self.tableWidget_scriptControls = self.create_table_widget( + parent=self.groupBox_scriptControls, + geometry=QtCore.QRect(10, 23, 132, 68), + font_name="Calibri", + font_size=9, + object_name="tableWidget_scriptControls", + column_count=1, + row_count=3 + ) + self.initialize_table_widget_items(self.tableWidget_scriptControls) + + def create_table_widget(self, parent, geometry, font_name, font_size, object_name, column_count, row_count): + table_widget = QtWidgets.QTableWidget(parent) + table_widget.setGeometry(geometry) + font = QtGui.QFont(font_name, font_size) + table_widget.setFont(font) + table_widget.setObjectName(object_name) + table_widget.setColumnCount(column_count) + table_widget.setRowCount(row_count) + + # Set properties based on the object name + if object_name == "tableWidget_scriptControls" or object_name == "tableWidget_kbmControls": + table_widget.horizontalHeader().setVisible(False) + + if object_name == "tableWidget_scriptControls": + table_widget.horizontalHeader().setDefaultSectionSize(50) + table_widget.verticalHeader().setDefaultSectionSize(20) + elif object_name == "tableWidget_ammoScripting": + table_widget.horizontalHeader().setDefaultSectionSize(66) + table_widget.verticalHeader().setDefaultSectionSize(20) + elif object_name == "tableWidget_kbmControls": + table_widget.horizontalHeader().setDefaultSectionSize(60) + table_widget.verticalHeader().setDefaultSectionSize(16) + + # Disable scrollbars + table_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + table_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + # Initialize table items with centered alignment and italic font + italic_font = QtGui.QFont(font_name, font_size) + italic_font.setItalic(True) + for row in range(row_count): + for col in range(column_count): + item = QtWidgets.QTableWidgetItem() + item.setFont(italic_font) + item.setTextAlignment(QtCore.Qt.AlignCenter) + table_widget.setItem(row, col, item) + + # Ensure header items exist + for row in range(row_count): + if not table_widget.verticalHeaderItem(row): + header_item = QtWidgets.QTableWidgetItem() + table_widget.setVerticalHeaderItem(row, header_item) + + for col in range(column_count): + if not table_widget.horizontalHeaderItem(col): + header_item = QtWidgets.QTableWidgetItem() + table_widget.setHorizontalHeaderItem(col, header_item) + + return table_widget + + def initialize_table_widget_items(self, table_widget): + if table_widget.objectName() == "tableWidget_ammoScripting": + column_options = { + 0: AMMO_NAMES, + 1: HOTKEYS_PS4, + 2: START_CONDITIONS, + 3: SHOT_LIMITS, + 4: FINISH_ACTIONS + } + + # Manually set column widths based on what you determine looks best + column_widths = [83, 62, 68, 49, 62] + + for row in range(table_widget.rowCount()): + for column in range(table_widget.columnCount()): + combo_box = QtWidgets.QComboBox() + combo_box.setStyleSheet("QComboBox { background-color: white; }") + + # Retrieve the options for the current column + options = column_options.get(column, []) + # Add the options to the combo box + for option in options: + combo_box.addItem(option) + + table_widget.setCellWidget(row, column, combo_box) + table_widget.setColumnWidth(column, column_widths[column]) + + elif table_widget.objectName() == "tableWidget_scriptControls": + row_options = { + 0: HOTKEYS_PC, + 1: HOTKEYS_PC, + 2: ["Joy7"] + } + + for row in range(table_widget.rowCount()): + combo_box = QtWidgets.QComboBox() + combo_box.setStyleSheet("QComboBox { background-color: white; }") + + # Retrieve the options for the current row + options = row_options.get(row, []) + # Add the options to the combo box + for option in options: + combo_box.addItem(option) + + # Set the combo box as the cell widget for the current row, column 0 + table_widget.setCellWidget(row, 0, combo_box) + + else: + # For other tables, use the standard initialization + for row in range(table_widget.rowCount()): + for column in range(table_widget.columnCount()): + # Check if the item already exists, if not, create it. + if not table_widget.item(row, column): + table_widget.setItem(row, column, QtWidgets.QTableWidgetItem()) + # Similarly, for header items, ensure they exist. + if not table_widget.verticalHeaderItem(row): + table_widget.setVerticalHeaderItem(row, QtWidgets.QTableWidgetItem()) + if not table_widget.horizontalHeaderItem(column): + table_widget.setHorizontalHeaderItem(column, QtWidgets.QTableWidgetItem()) + + def finalize_setup(self, LLBG): + self.retranslateUi(LLBG) + self.buttonBoxes.accepted.connect(LLBG.accept) + self.buttonBoxes.rejected.connect(LLBG.reject) + QtCore.QMetaObject.connectSlotsByName(LLBG) + + + def retranslateUi(self, LLBG): + _translate = QtCore.QCoreApplication.translate + LLBG.setWindowTitle(_translate("LLBG", "Ledge Light Bowgun (LLBG) Exploit Tool")) + icon = QtGui.QIcon(self.icon_filePath) + LLBG.setWindowIcon(icon) + self.setTitles(_translate) + self.setAmmoScriptingTableHeaders(_translate) + self.setKbmControlsTableHeaders(_translate) + self.setKbmControlsTableItems(_translate) + self.setScriptControlsTableHeaders(_translate) + self.setScriptControlsTableItems(_translate) + self.setNotes(_translate) + + def setTitles(self, _translate): + self.groupBox_ammoScripting.setTitle(_translate("LLBG", "📜 Ammo Scripting")) + self.groupBox_general.setTitle(_translate("LLBG", "⚙ī¸ General")) + self.groupBox_kbmControls.setTitle(_translate("LLBG", "⌨ī¸ KbM Controls")) + self.groupBox_scriptControls.setTitle(_translate("LLBG", "🕹ī¸ Script Controls")) + self.label_primaryHotkey.setText(_translate("LLBG", "Primary Ammo Hotkey:")) + self.label_AmmoSyncHotkey.setText(_translate("LLBG", "Ammo Sync Hotkey:")) + self.checkBox_AmmoSync.setText(_translate("LLBG", "Automatic Ammo Syncs")) + self.checkBox_deepBounces.setText(_translate("LLBG", "Deeper Bounces")) + self.checkBox_fpsDipReload.setText(_translate("LLBG", "FPS Dip Reloads")) + + self.label_lbgName.setText(_translate("LLBG", "LBG Name:")) + self.label_primaryAmmo.setText(_translate("LLBG", "Primary Ammo:")) + + def setAmmoScriptingTableHeaders(self, _translate): + headers = ["Ammo Name", "Queue Hotkey", "Start Condition", "Shot Limit", "Finish Action"] + for i, header in enumerate(headers): + item = self.tableWidget_ammoScripting.horizontalHeaderItem(i) + item.setText(_translate("LLBG", header)) + for i in range(5): + item = self.tableWidget_ammoScripting.verticalHeaderItem(i) + item.setText(_translate("LLBG", str(i + 1))) + + def setKbmControlsTableHeaders(self, _translate): + self.tableWidget_kbmControls.horizontalHeaderItem(0).setText(_translate("LLBG", "Key")) + labels = [ + "Walk âŦ†ī¸", "Walk âŦ‡ī¸", "Reload", "Roll", "Ammo đŸ”ŧ", "Ammo đŸ”Ŋ", + "Item ←", "Item →", "Fire", "Aim", "Sp. Atk", "Craft" + ] + for i, label in enumerate(labels): + item = self.tableWidget_kbmControls.verticalHeaderItem(i) + item.setText(_translate("LLBG", label)) + + def setKbmControlsTableItems(self, _translate): + keys = ["w", "s", "Xbutton1", "space", "up", "down", "left", "right", "Lbutton", "Rbutton", "Xbutton2"] + for i, key in enumerate(keys): + item = self.tableWidget_kbmControls.item(i, 0) + item.setText(_translate("LLBG", key)) + + def setScriptControlsTableHeaders(self, _translate): + self.tableWidget_scriptControls.horizontalHeaderItem(0).setText(_translate("LLBG", "Hotkey")) + labels = ["Reload Script", "Stop Script", "Exit LLBG"] + for i, label in enumerate(labels): + item = self.tableWidget_scriptControls.verticalHeaderItem(i) + item.setText(_translate("LLBG", label)) + + def setScriptControlsTableItems(self, _translate): + hotkeys = ["F6", "F10", "Joy7"] + for i, hotkey in enumerate(hotkeys): + if hotkey: + comboBox = self.tableWidget_scriptControls.cellWidget(i, 0) + if isinstance(comboBox, QtWidgets.QComboBox): + # Find the index of the hotkey in the combo box and set it as the current index + index = comboBox.findText(_translate("LLBG", hotkey)) + if index >= 0: + comboBox.setCurrentIndex(index) + + def save_settings(self, no_confirmation=False): + settings = { + 'LBG Name': self.comboBox_lbgName.currentText(), + 'Primary Ammo': self.comboBox_primaryAmmo.currentText(), + 'Deeper Bounces' : self.checkBox_deepBounces.isChecked(), + 'FPS Dip Reloads' : self.checkBox_fpsDipReload.isChecked(), + 'keyScriptReload': self.tableWidget_scriptControls.cellWidget(0, 0).currentText(), + 'keyScriptExit': self.tableWidget_scriptControls.cellWidget(1, 0).currentText(), + + 'Ammo Script Enabled' : self.groupBox_ammoScripting.isChecked(), + 'Primary Hotkey' : self.comboBox_primaryHotkey.currentText(), + 'Ammo Sync Hotkey' : self.comboBox_AmmoSyncHotkey.currentText(), + 'Auto Ammo Sync' : self.checkBox_AmmoSync.isChecked() + + } + # Add KbM Controls table settings + kbm_control_keys = [ + 'keyForward', + 'keyBack', + 'keyReload', + 'keyRoll', + 'keyAmmoUp', + 'keyAmmoDown', + 'keyItemLeft', + 'keyItemRight', + 'keyFire', + 'keyADS', + 'keyMine', + 'keyCraft', + ] + for i, key in enumerate(kbm_control_keys): + item = self.tableWidget_kbmControls.item(i, 0) + settings[key] = item.text() if item else "" + + # Add Ammo Script table settings + for i in range(5): + for j in range(5): + comboBox = self.tableWidget_ammoScripting.cellWidget(i, j) + header = self.tableWidget_ammoScripting.horizontalHeaderItem(j) + key = f'{i+1} {header.text()}' if header else f'{i+1} Column{j}' + settings[key] = comboBox.currentText() if comboBox else "" + + with open(self.settings_filePath, 'w') as file: + json.dump(settings, file, indent=4) + + # Display a message box confirming the save + if not no_confirmation: + # Display a message box confirming the save if show_confirmation is True + QtWidgets.QMessageBox.information(None, "Settings Saved", "Your settings have been successfully saved.") + + # Press ahk hotkey to reload script (if running) + pyautogui.press(settings['keyScriptReload']) + + def load_settings(self): + try: + with open(self.settings_filePath, 'r') as file: + settings = json.load(file) + + # Load combo box and check box settings + self.comboBox_lbgName.setCurrentText(settings.get('LBG Name', "")) + self.comboBox_primaryAmmo.setCurrentText(settings.get('Primary Ammo', "")) + self.checkBox_deepBounces.setChecked(settings.get('Deeper Bounces', False)) + self.checkBox_fpsDipReload.setChecked(settings.get('FPS Dip Reloads', False)) + + # Load settings for QTableWidget with QComboBoxes + self.tableWidget_scriptControls.cellWidget(0, 0).setCurrentText(settings.get('keyScriptReload', "")) + self.tableWidget_scriptControls.cellWidget(1, 0).setCurrentText(settings.get('keyScriptExit', "")) + + # Load group box check state + self.groupBox_ammoScripting.setChecked(settings.get('Ammo Script Enabled', False)) + + # Load other combo box settings + self.comboBox_primaryHotkey.setCurrentText(settings.get('Primary Hotkey', "")) + self.comboBox_AmmoSyncHotkey.setCurrentText(settings.get('Ammo Sync Hotkey', "")) + self.checkBox_AmmoSync.setChecked(settings.get('Auto Ammo Sync', False)) + + # Load KbM Controls table settings + kbm_control_keys = [ + 'keyForward', + 'keyBack', + 'keyReload', + 'keyRoll', + 'keyAmmoUp', + 'keyAmmoDown', + 'keyItemLeft', + 'keyItemRight', + 'keyFire', + 'keyADS', + 'keyMine', + 'keyCraft', + ] + for i, key in enumerate(kbm_control_keys): + if self.tableWidget_kbmControls.item(i, 0) is not None: + self.tableWidget_kbmControls.item(i, 0).setText(settings.get(key, "")) + + # Load Ammo Script table settings + for i in range(5): + for j in range(5): + comboBox = self.tableWidget_ammoScripting.cellWidget(i, j) + if comboBox: + header = self.tableWidget_ammoScripting.horizontalHeaderItem(j) + key = f'{i+1} {header.text()}' if header else f'{i+1} Column{j}' + comboBox.setCurrentText(settings.get(key, "")) + + except (FileNotFoundError, json.JSONDecodeError) as e: + if isinstance(e, json.JSONDecodeError): + # If there is a JSON decode error, warn the user + QtWidgets.QMessageBox.warning(None, "Load Error", "Could not decode the settings file.") + # Restore defaults and save them without confirmation or reloading AHK + self.restore_defaults(no_confirmation=True) + self.save_settings(no_confirmation=True) + + def restore_defaults(self, no_confirmation=False): + # Check if we should ask for confirmation + if not no_confirmation: + reply = QtWidgets.QMessageBox.question( + None, + 'Restore Defaults', + 'Are you sure you want to restore to the default settings?', + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No + ) + if reply != QtWidgets.QMessageBox.Yes: + return # Do not restore defaults if user chooses not to + + # Reset combo box and check box settings to default values + self.comboBox_lbgName.setCurrentIndex(0) + self.comboBox_primaryAmmo.setCurrentIndex(0) + self.checkBox_deepBounces.setChecked(False) + self.checkBox_fpsDipReload.setChecked(False) + + # Reset Script Controls table settings to default hotkeys + default_hotkeys = ["F6", "F10"] + for i, hotkey in enumerate(default_hotkeys): + comboBox = self.tableWidget_scriptControls.cellWidget(i, 0) + if isinstance(comboBox, QtWidgets.QComboBox): + index = comboBox.findText(hotkey) # Removed the _translate for simplicity + if index >= 0: + comboBox.setCurrentIndex(index) + else: + comboBox.setCurrentIndex(0) # Set to the first item if the default is not found + + # Reset group box check state to default + self.groupBox_ammoScripting.setChecked(False) + + # Reset other combo box settings to default values + self.comboBox_primaryHotkey.setCurrentIndex(12) + self.comboBox_AmmoSyncHotkey.setCurrentIndex(13) + self.checkBox_AmmoSync.setChecked(False) + + # Reset KbM Controls table settings to default key bindings + default_keys = ["w", "s", "Xbutton1", "space", "up", "down", "left", "right", "Lbutton", "Rbutton", "Xbutton2"] + for i, key in enumerate(default_keys): + item = self.tableWidget_kbmControls.item(i, 0) + if item is None: + item = QtWidgets.QTableWidgetItem() + self.tableWidget_kbmControls.setItem(i, 0, item) + item.setText(key) # Removed the _translate for simplicity + + # Reset Ammo Script table settings to default values + for i in range(self.tableWidget_ammoScripting.rowCount()): + for j in range(self.tableWidget_ammoScripting.columnCount()): + comboBox = self.tableWidget_ammoScripting.cellWidget(i, j) + if comboBox: + comboBox.setCurrentIndex(0) + + def setNotes(self, _translate): + self.label_primaryHotkeyNote.setText(_translate("LLBG", "(Also works outside LLBG)")) + + def open_ps4_joy_map(self): + # Create a dialog to display the image + self.imageDialog = QtWidgets.QDialog() + self.imageDialog.setWindowTitle("🎮 PS4 Joy Map") + + # Set window flags to remove the title bar icon + self.imageDialog.setWindowFlags( + QtCore.Qt.Dialog | + QtCore.Qt.CustomizeWindowHint | + QtCore.Qt.WindowTitleHint | + QtCore.Qt.WindowCloseButtonHint + ) + + # Create a label to hold the image + label = QtWidgets.QLabel(self.imageDialog) + pixmap = QtGui.QPixmap(self.ps4joy_filePath) + label.setPixmap(pixmap) + label.setScaledContents(True) + + # Set the dialog size to match the image size + self.imageDialog.setFixedSize(pixmap.size()) + + # Use a layout to contain the label + layout = QtWidgets.QVBoxLayout() + layout.addWidget(label) + self.imageDialog.setLayout(layout) + + # Show the image dialog + self.imageDialog.exec_() # Use exec_() to make the dialog modal + + def run_script(self): + # Save the current directory + original_dir = os.getcwd() + + try: + # Convert the relative path to an absolute path + abs_script_path = os.path.abspath(self.runLLBG_filePath) + + + # Check if the script exists before attempting to run it + if os.path.isfile(abs_script_path): + # Change to the directory where the VBScript is located + os.chdir(os.path.dirname(abs_script_path)) + + # Execute the VBScript while setting the cwd to the script's directory + subprocess.Popen(['cscript', '//nologo', os.path.basename(abs_script_path)], shell=True, creationflags=subprocess.CREATE_NO_WINDOW) + else: + error_msg = f"The script run_llbg.vbs was not found in the directory: {os.path.dirname(abs_script_path)}" + print(error_msg) + QtWidgets.QMessageBox.critical(None, "Error", error_msg) + except Exception as e: + error_msg = f"An error occurred: {e}" + print(error_msg) + QtWidgets.QMessageBox.critical(None, "Error", error_msg) + finally: + # Restore the original directory + os.chdir(original_dir) + + def stop_script(self): + # Press ahk hotkey to stop script (if running) + pyautogui.press(self.tableWidget_scriptControls.cellWidget(1, 0).currentText()) + + def list_ammo_contents(self): + # Convert the relative path to an absolute path + abs_file_path = os.path.abspath(self.ammoList_filePath) + + # Check if the INI file exists + if os.path.isfile(abs_file_path): + # Read the INI file with UTF-16 encoding + config = ConfigParser() + try: + with open(abs_file_path, 'r', encoding='utf-16') as f: + config.read_file(f) + + # Check if 'AmmoList' section exists + if 'AmmoList' in config: + ammo_list = [] + for num, key in enumerate(config['AmmoList'], 1): + # Translate the number to its corresponding ammo name + ammo_name = self.ammo_mapping.get(config['AmmoList'][key], "Unknown Ammo") + ammo_list.append(f"{num}. {ammo_name}") + ammo_str = "\n".join(ammo_list) + + # Show ammo list in a message box without an icon and less padding + self.show_custom_message("đŸ’ŧ Syncd Ammo List", ammo_str) + else: + self.show_custom_message("⚠ī¸ Warning", "Synchronization pending: no ammo data found.") + except UnicodeError: + self.show_custom_message("Error", "The ammo list file has an unsupported encoding.", critical=True) + except ConfigParser.MissingSectionHeaderError: + self.show_custom_message("Error", "The ammo list file is missing section headers.", critical=True) + else: + self.show_custom_message("⚠ī¸ Warning", "Synchronization pending: no ammo data found.") + + def show_custom_message(self, title, message, critical=False): + msgBox = QtWidgets.QMessageBox() + msgBox.setWindowTitle(title) + msgBox.setText(message) + msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok) + + # Customize window flags to remove the title bar icon + msgBox.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) + + if critical: + msgBox.setIcon(QtWidgets.QMessageBox.Critical) + else: + msgBox.setIcon(QtWidgets.QMessageBox.NoIcon) + + msgBox.exec_() + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + LLBG = QtWidgets.QDialog() + + ui = Ui_LLBG() + ui.setupUi(LLBG) + + # Define a close event handler function + def closeEvent(event): + ui.stop_script() + QtWidgets.QDialog.closeEvent(LLBG, event) + + # Assign the close event handler to the QDialog instance + LLBG.closeEvent = closeEvent + + LLBG.show() + sys.exit(app.exec_()) + diff --git a/MHWI_LLBG/gui/ps4joy.png b/MHWI_LLBG/gui/ps4joy.png new file mode 100644 index 0000000..45ccb32 Binary files /dev/null and b/MHWI_LLBG/gui/ps4joy.png differ diff --git a/MHWI_LLBG/gui/view_gui.py.bat b/MHWI_LLBG/gui/view_gui.py.bat new file mode 100644 index 0000000..448e8dc --- /dev/null +++ b/MHWI_LLBG/gui/view_gui.py.bat @@ -0,0 +1,14 @@ +@echo off +echo Running Python script... + +REM Change directory two levels up (this is where exe will reside) +pushd ..\.. + +REM Run the Python script which is located in the 'gui' subdirectory +python MHWI_LLBG\gui\gui.py + +REM Return to the original directory +popd + +echo Script finished. +pause diff --git a/MHWI_LLBG/lib/Functions.ahk b/MHWI_LLBG/lib/Functions.ahk new file mode 100644 index 0000000..68a8a92 --- /dev/null +++ b/MHWI_LLBG/lib/Functions.ahk @@ -0,0 +1,149 @@ +īģŋ;;==============================[Timing Functions]============================= +;;Precision Sleep using DllCall +PreciseSleep(duration) { + DllCall("Sleep","UInt",duration) +} + +;;Precise Sleep using the QueryPerformanceCounter +QPCsleep(value) { + static Frequency := 0 + if !Frequency + DllCall("QueryPerformanceFrequency", "Int64*", &Frequency) + Start := 0 + DllCall("QueryPerformanceCounter", "Int64*", &Start) + Finish := Start + (Frequency * (value / 1000)) + loop { + Current := 0 + DllCall("QueryPerformanceCounter", "Int64*", &Current) + } until (Current >= Finish) + return +} + +; Using Query Performance as a timer: QPC() behaves like a more precise A_TickCount +QPC() { + static Freq := 0 + static init := DllCall("QueryPerformanceFrequency", "Int64*", &Freq) + Count := 0 + DllCall("QueryPerformanceCounter", "Int64*", &Count) + Return (Count / Freq) * 1000 +} + +;;===============================[Kill All AHK]================================ +AHKPanic(Kill:=0, Pause:=0, Suspend:=0, SelfToo:=0) { + DetectHiddenWindows(true) + IDList := WinGetList("ahk_class AutoHotkey") + for index, ID in IDList + { + ATitle := WinGetTitle("ahk_id " ID) + if !InStr(ATitle, A_ScriptFullPath) + { + if Suspend + PostMessage(0x111, 65305, , , "ahk_id " ID) ; Suspend. + if Pause + PostMessage(0x111, 65306, , , "ahk_id " ID) ; Pause. + if Kill + WinClose("ahk_id " ID) ; Kill. + } + } + if SelfToo + { + if Suspend + Suspend('Toggle') ; Suspend. + if Pause + Pause('Toggle') ; Pause. + if Kill + ExitApp() + } +} + +; =============================[Custom Functions]============================== +Pressed(key) { + return GetKeyState(key, "P") +} + +KeyActive(key) { + return GetKeyState(key) +} + +SendKey(keyVar, state:="Down") { + ; Check if keyVar is an array + if (type(keyVar) = "Array") { + keysToSend := "" + for key in keyVar { + keysToSend .= "{" key " " state "}" + } + Send keysToSend + } else { + Send "{" keyVar " " state "}" + } +} + +TimedPulse(Keys, PulseTime, OnDelay:=0, OffDelay:=0) { + ; Ensure the Keys variable is an array. If it's not, make it one. + if (type(Keys) != "Array") + Keys := [Keys] + + if (OnDelay > 0) + QPCsleep(OnDelay) + + ; Send the keys down using SendKey function. + for Key in Keys { + SendKey(Key) ; Default is Down + } + + QPCsleep(PulseTime) + + ; Send the keys up using SendKey function. + for Key in Keys { + SendKey(Key, "Up") + } + + if (OffDelay > 0) + QPCsleep(OffDelay) +} + +ClearKeyStates(keysArray) { + for key in keysArray { + if (GetKeyState(key)) + SendKey(key, "Up") + } +} + +getArrayValueIndex(arr, val) { + Loop arr.Length { + if (arr[A_Index] == val) + return A_Index + } +} + +ini2Array(filePath, sectionName) { + valuesArray := [] + if (FileExist(filePath) && (sectionContent := IniRead(filePath, sectionName)) != "") { + for each, line in StrSplit(sectionContent, "`n", "`r") { + if ((SplitLine := StrSplit(line, "=")).Length > 1) + valuesArray.Push(SplitLine[2]) + } + } + return valuesArray +} + +Array2Ini(filePath, section, array) { + try { + FileDelete(filePath) + } + for index, value in array { + IniWrite(value, filePath, section, index) + } +} + +TestSound() { + SoundPlay "*-1" ; Generates a simple beep. +} + +OnSound() { + SoundPlay(A_ScriptDir "\sounds\scriptOn.wav") +} + +OffSound() { + SoundPlay(A_ScriptDir "\sounds\scriptOff.wav") +} \ No newline at end of file diff --git a/MHWI_LLBG/lib/Initialization.ahk b/MHWI_LLBG/lib/Initialization.ahk new file mode 100644 index 0000000..8372a8d --- /dev/null +++ b/MHWI_LLBG/lib/Initialization.ahk @@ -0,0 +1,16 @@ +īģŋ;;==============================[Initialization]================================ +;;Most of these settings are for script performance +;#Requires AutoHotkey v2.0 +#SingleInstance Force +#MaxThreads 10 +ProcessSetPriority "High" +SetKeyDelay -1, -1 +SetMouseDelay -1 +SetDefaultMouseSpeed 0 +SetWinDelay -1 +SetControlDelay -1 +SendMode "Input" +ListLines 0 +DetectHiddenWindows true +SetWorkingDir A_ScriptDir +ScriptName := SubStr(A_ScriptName, InStr(A_ScriptName, "\",, -1) + 1) \ No newline at end of file diff --git a/MHWI_LLBG/lib/MemoryLibrary.ahk b/MHWI_LLBG/lib/MemoryLibrary.ahk new file mode 100644 index 0000000..52c40a0 --- /dev/null +++ b/MHWI_LLBG/lib/MemoryLibrary.ahk @@ -0,0 +1,200 @@ +īģŋ;;==============================[MHWI MemReadInfo]============================= +global prcName := "MonsterHunterWorld.exe" +global bAddress := 0x140000000 +global prcId := Memory_GetProcessID(prcName) +global prcHandle := Memory_GetProcessHandle(prcId) + +global stateOffsets := [0x500AB60, 0x80, 0x6278] +global stateAddress := GetAddressPtrChain(stateOffsets) ;Action ID +global animIDOffsets := [0x500AB60, 0x80, 0x468, 0x238] +global animIDAddress := GetAddressPtrChain(animIDOffsets) ;Primary Animation ID +global animID2Offsets := [0x500AB60, 0x80, 0x468, 0x27A8] +global animID2Address := GetAddressPtrChain(animID2Offsets) ;Secondary Animation ID + +global ammoOffsets := [0x500AB60, 0x80, 0x76B0, 0x23A8] +global ammoAddress := GetAddressPtrChain(ammoOffsets) +global ammoTypeOffsets := [0x500AB60, 0x80, 0x76B0, 0x27C8] +global ammoTypeAddress := GetAddressPtrChain(ammoTypeOffsets) + +global ammoMenuOffsets := [0x050EA510, 0x110, 0x98, 0x08, 0x2C0, 0x388, 0x160, 0xFB8] +global ammoMenuAddress := GetAddressPtrChain(ammoMenuOffsets) + +global frameRateOffsets := [0x051C1F08, 0x5C] +global frameRateAddress := GetAddressPtrChain(frameRateOffsets) + +;;===========================[MHW Mem ReadFunctions]=========================== +;;Run to rescan ptr addresses +ReScanPtrs(){ + global stateAddress := GetAddressPtrChain(stateOffsets) + global animIDAddress := GetAddressPtrChain(animIDOffsets) + global animID2Address := GetAddressPtrChain(animID2Offsets) + global ammoAddress := GetAddressPtrChain(ammoOffsets) + global ammoTypeAddress := GetAddressPtrChain(ammoTypeOffsets) + global ammoMenuAddress := GetAddressPtrChain(ammoMenuOffsets) + global frameRateAddress := GetAddressPtrChain(frameRateOffsets) +} + +;;Get the Action ID of your character +GetActionID() { + return Memory_Read(prcHandle, stateAddress) +} + +;;Get the Primary Animation ID of your character. +GetAnimID() { + return Memory_Read(prcHandle, animIDAddress) +} + +;;Get the Secondary Animation ID of your character. +GetAnim2ID() { + return Memory_Read(prcHandle, animID2Address) +} + +;;Update Ammo Type +GetAmmoType() { + return Memory_Read(prcHandle, ammoTypeAddress) +} + +;;Get Ammo Mag Count for specific ammo type +GetAmmoCount(type) { + return Memory_Read(prcHandle, ammoAddress + 0x18 * type) +} + +;;Get Ammo Clip Count for specific ammo type +GetAmmoCount2(type) { + return Memory_Read(prcHandle, ammoAddress + (0x18 * type) + 0x04) +} + +;;Return the ammo count of the current ammo type +GetCurrentAmmoCount(){ + return GetAmmoCount(GetAmmoType()) +} + +;;Returns True is ammo menu is open, else false +GetAmmoMenuState(){ + return !(Memory_Read(prcHandle, ammoMenuAddress) & 0x00FF) +} + +global ammoNameToID := Map( + "Normal 1", 0, "Normal 2", 1, "Normal 3", 2, "Pierce 1", 3, "Pierce 2", 4, + "Pierce 3", 5, "Spread 1", 6, "Spread 2", 7, "Spread 3", 8, "Cluster 1", 9, + "Cluster 2", 10, "Cluster 3", 11, "Sticky 1", 13, "Sticky 2", 14, "Sticky 3", 15, + "Slicing", 16, "Flaming", 17, "Water", 18, "Freeze", 19, "Thunder", 20, + "Dragon Ammo", 21, "Poison 1", 22, "Poison 2", 23, "Paralysis 1", 24, + "Paralysis 2", 25, "Sleep 1", 26, "Sleep 2", 27, "Exhaust 1", 28, + "Exhaust 2", 29, "Recovery 1", 30, "Recovery 2", 31, "Demon", 32, + "Armor", 33, "Tranq", 36 +) +global ammoIDToName := Map() + +; Populate the reverse map +for ammoName, ammoId in ammoNameToID { + ammoIDToName[ammoId] := ammoName +} + +;; Return an ammo ID given a string +GetAmmoID(name) { + value := ammoNameToID[name] + return (value != "") ? value : -1 +} + +;; Return the ammo name given an ID +GetAmmoName(id) { + return ammoIDToName[id] ? ammoIDToName[id] : "" +} + +; Get the current set framerate +GetFPS() { + return Memory_ReadFloat(prcHandle, frameRateAddress) +} + +; Set the framerate +SetFPS(fps) { + Memory_WriteFloat(prcHandle, frameRateAddress, fps) +} + +;;==============================[Memory Library]=============================== +;;Check if current active window is correct +CheckWindow() { + return WinActive("ahk_exe " . prcName) +} + +;;Runs clean up tasks prior to exiting +CleanExit() { + Memory_CloseHandle(prcHandle) +} + +;;Based on: https://github.com/kevrgithub/autohotkey/blob/master/Lib/Memory.ahk + +Memory_GetProcessID(process_name) { + PID := ProcessExist(process_name) + return PID +} + +Memory_GetProcessHandle(process_id) { + return DllCall("OpenProcess", "UInt", 0x001F0FFF, "Int", false, "UInt", process_id, "Ptr") ; PROCESS_ALL_ACCESS +} + +Memory_CloseHandle(process_handle) { + DllCall("CloseHandle", "Ptr", process_handle) +} + +Memory_Read(process_handle, address, t:="UInt", size:=4) { + + + if (t = "UInt" and size = 4) + { + if (DllCall('ReadProcessMemory', 'UInt', process_handle, 'UInt', address, 'UInt*', &value := 0, 'Ptr', size, 'UInt', 0)) + { + return value + } + } + else if (t = "Int64" and size = 8) + { + if (DllCall('ReadProcessMemory', 'UInt', process_handle, 'UInt', address, 'Int64*', &value := 0, 'Ptr', size, 'UInt', 0)) + { + return value + } + } + + return False ; Return a default value in case of failure +} + +Memory_ReadFloat(process_handle, address) { + value := Buffer(4) + DllCall("ReadProcessMemory", "Ptr", process_handle, "Ptr", address, "Ptr", value.Ptr, "UInt", 4, "UPtrP", 0) + return NumGet(value, "Float") +} + +Memory_WriteFloat(process_handle, address, floatValue) { + ; Allocate a buffer for a 32-bit value (which can hold a float) + buf := Buffer(4) + ; Write the float value into the buffer as a 32-bit float + NumPut("Float", floatValue, buf, 0) + + ; Change memory protection to PAGE_READWRITE if necessary + oldProtect := 0 + DllCall("VirtualProtectEx", "Ptr", process_handle, "Ptr", address, "UInt", 4, "UInt", 0x04, "PtrP", oldProtect) + + ; Write the float value to memory + DllCall("WriteProcessMemory", "Ptr", process_handle, "Ptr", address, "Ptr", buf, "UInt", 4, "UPtrP", 0) +} + +;;Get final target address of a pointer chain +GetAddressPtrChain(ptrOffsets){ + GetArrayLength(arr) { + count := 0 + for _, _ in arr + count++ + return count + } + address := bAddress + last := GetArrayLength(ptrOffsets) + for k,v in ptrOffsets + { + address := address + v + if (k != last) { + address := Memory_Read(prcHandle, address, "Int64", 8) + } + } + return address +} \ No newline at end of file diff --git a/MHWI_LLBG/lib/_JXON.ahk b/MHWI_LLBG/lib/_JXON.ahk new file mode 100644 index 0000000..9a1738c --- /dev/null +++ b/MHWI_LLBG/lib/_JXON.ahk @@ -0,0 +1,208 @@ +īģŋ;;;; AHK v2 https://github.com/TheArkive/JXON_ahk2 +; Example =================================================================================== +; =========================================================================================== + +; Msgbox "The idea here is to create several nested arrays, save to text with jxon_dump(), and then reload the array with jxon_load(). The resulting array should be the same.`r`n`r`nThis is what this example shows." +; a := Map(), b := Map(), c := Map(), d := Map(), e := Map(), f := Map() ; Object() is more technically correct than {} but both will work. + +; d["g"] := 1, d["h"] := 2, d["i"] := ["purple","pink","pippy red"] +; e["g"] := 1, e["h"] := 2, e["i"] := Map("1","test1","2","test2","3","test3") +; f["g"] := 1, f["h"] := 2, f["i"] := [1,2,Map("a",1.0009,"b",2.0003,"c",3.0001)] + +; a["test1"] := "test11", a["d"] := d +; b["test3"] := "test33", b["e"] := e +; c["test5"] := "test55", c["f"] := f + +; myObj := Map() +; myObj["a"] := a, myObj["b"] := b, myObj["c"] := c, myObj["test7"] := "test77", myObj["test8"] := "test88" + +; g := ["blue","green","red"], myObj["h"] := g ; add linear array for testing + +; q := Chr(34) +; textData2 := Jxon_dump(myObj,4) ; ===> convert array to JSON +; msgbox "JSON output text:`r`n===========================================`r`n(Should match second output.)`r`n`r`n" textData2 + +; newObj := Jxon_load(&textData2) ; ===> convert json back to array + +; textData3 := Jxon_dump(newObj,4) ; ===> break down array into 2D layout again, should be identical +; msgbox "Second output text:`r`n===========================================`r`n(should be identical to first output)`r`n`r`n" textData3 + +; msgbox "textData2 = textData3: " ((textData2=textData3) ? "true" : "false") + +; =========================================================================================== +; End Example ; ============================================================================= +; =========================================================================================== + +; originally posted by user coco on AutoHotkey.com +; https://github.com/cocobelgica/AutoHotkey-JSON + +Jxon_Load(&src, args*) { + key := "", is_key := false + stack := [ tree := [] ] + next := '"{[01234567890-tfn' + pos := 0 + + while ( (ch := SubStr(src, ++pos, 1)) != "" ) { + if InStr(" `t`n`r", ch) + continue + if !InStr(next, ch, true) { + testArr := StrSplit(SubStr(src, 1, pos), "`n") + + ln := testArr.Length + col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) + + msg := Format("{}: line {} col {} (char {})" + , (next == "") ? ["Extra data", ch := SubStr(src, pos)][1] + : (next == "'") ? "Unterminated string starting at" + : (next == "\") ? "Invalid \escape" + : (next == ":") ? "Expecting ':' delimiter" + : (next == '"') ? "Expecting object key enclosed in double quotes" + : (next == '"}') ? "Expecting object key enclosed in double quotes or object closing '}'" + : (next == ",}") ? "Expecting ',' delimiter or object closing '}'" + : (next == ",]") ? "Expecting ',' delimiter or array closing ']'" + : [ "Expecting JSON value(string, number, [true, false, null], object or array)" + , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1] + , ln, col, pos) + + throw Error(msg, -1, ch) + } + + obj := stack[1] + is_array := (obj is Array) + + if i := InStr("{[", ch) { ; start new object / map? + val := (i = 1) ? Map() : Array() ; ahk v2 + + is_array ? obj.Push(val) : obj[key] := val + stack.InsertAt(1,val) + + next := '"' ((is_key := (ch == "{")) ? "}" : "{[]0123456789-tfn") + } else if InStr("}]", ch) { + stack.RemoveAt(1) + next := (stack[1]==tree) ? "" : (stack[1] is Array) ? ",]" : ",}" + } else if InStr(",:", ch) { + is_key := (!is_array && ch == ",") + next := is_key ? '"' : '"{[0123456789-tfn' + } else { ; string | number | true | false | null + if (ch == '"') { ; string + i := pos + while i := InStr(src, '"',, i+1) { + val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C") + if (SubStr(val, -1) != "\") + break + } + if !i ? (pos--, next := "'") : 0 + continue + + pos := i ; update pos + + val := StrReplace(val, "\/", "/") + val := StrReplace(val, '\"', '"') + , val := StrReplace(val, "\b", "`b") + , val := StrReplace(val, "\f", "`f") + , val := StrReplace(val, "\n", "`n") + , val := StrReplace(val, "\r", "`r") + , val := StrReplace(val, "\t", "`t") + + i := 0 + while i := InStr(val, "\",, i+1) { + if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0 + continue 2 + + xxxx := Abs("0x" . SubStr(val, i+2, 4)) ; \uXXXX - JSON unicode escape sequence + if (xxxx < 0x100) + val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6) + } + + if is_key { + key := val, next := ":" + continue + } + } else { ; number | true | false | null + val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) + + if IsInteger(val) + val += 0 + else if IsFloat(val) + val += 0 + else if (val == "true" || val == "false") + val := (val == "true") + else if (val == "null") + val := "" + else if is_key { + pos--, next := "#" + continue + } + + pos += i-1 + } + + is_array ? obj.Push(val) : obj[key] := val + next := obj == tree ? "" : is_array ? ",]" : ",}" + } + } + + return tree[1] +} + +Jxon_Dump(obj, indent:="", lvl:=1) { + if IsObject(obj) { + If !(obj is Array || obj is Map || obj is String || obj is Number) + throw Error("Object type not supported.", -1, Format("", ObjPtr(obj))) + + if IsInteger(indent) + { + if (indent < 0) + throw Error("Indent parameter must be a postive integer.", -1, indent) + spaces := indent, indent := "" + + Loop spaces ; ===> changed + indent .= " " + } + indt := "" + + Loop indent ? lvl : 0 + indt .= indent + + is_array := (obj is Array) + + lvl += 1, out := "" ; Make #Warn happy + for k, v in obj { + if IsObject(k) || (k == "") + throw Error("Invalid object key.", -1, k ? Format("", ObjPtr(obj)) : "") + + if !is_array ;// key ; ObjGetCapacity([k], 1) + out .= (ObjGetCapacity([k]) ? Jxon_Dump(k) : escape_str(k)) (indent ? ": " : ":") ; token + padding + + out .= Jxon_Dump(v, indent, lvl) ; value + . ( indent ? ",`n" . indt : "," ) ; token + indent + } + + if (out != "") { + out := Trim(out, ",`n" . indent) + if (indent != "") + out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) + } + + return is_array ? "[" . out . "]" : "{" . out . "}" + + } Else If (obj is Number) + return obj + + Else ; String + return escape_str(obj) + + escape_str(obj) { + obj := StrReplace(obj,"\","\\") + obj := StrReplace(obj,"`t","\t") + obj := StrReplace(obj,"`r","\r") + obj := StrReplace(obj,"`n","\n") + obj := StrReplace(obj,"`b","\b") + obj := StrReplace(obj,"`f","\f") + obj := StrReplace(obj,"/","\/") + obj := StrReplace(obj,'"','\"') + + return '"' obj '"' + } +} + diff --git a/MHWI_LLBG/run_llbg.vbs b/MHWI_LLBG/run_llbg.vbs new file mode 100644 index 0000000..591b2a6 --- /dev/null +++ b/MHWI_LLBG/run_llbg.vbs @@ -0,0 +1,11 @@ +Set WshShell = CreateObject("WScript.Shell") + +' Construct the full path for the executable and the script file +Dim exePath, scriptPath +exePath = WshShell.CurrentDirectory & "\AutoHotkey.exe" +scriptPath = WshShell.CurrentDirectory & "\MHWI_LLBG.ahk" + +' Run the executable with the script file as a parameter +WshShell.Run """" & exePath & """ """ & scriptPath & """", 0, False + +Set WshShell = Nothing diff --git a/MHWI_LLBG/sounds/scriptOff.wav b/MHWI_LLBG/sounds/scriptOff.wav new file mode 100644 index 0000000..b5d9712 Binary files /dev/null and b/MHWI_LLBG/sounds/scriptOff.wav differ diff --git a/MHWI_LLBG/sounds/scriptOn.wav b/MHWI_LLBG/sounds/scriptOn.wav new file mode 100644 index 0000000..01c8188 Binary files /dev/null and b/MHWI_LLBG/sounds/scriptOn.wav differ