|
|
|
|
|
(function() { |
|
|
|
|
|
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent); |
|
|
const isTouch = isMobile && window.matchMedia('(pointer: coarse)').matches; |
|
|
|
|
|
const style = document.createElement('style'); |
|
|
style.textContent = ` |
|
|
#cheat-engine-ui { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
right: 0; |
|
|
width: ${isTouch ? '100vw' : '340px'}; |
|
|
height: 100vh; |
|
|
background: rgba(10, 10, 10, 0.95); |
|
|
backdrop-filter: blur(15px); |
|
|
color: #eee; |
|
|
font-family: 'Consolas', 'Monaco', monospace; |
|
|
padding: ${isTouch ? '15px 15px 150px 15px' : '20px 20px 100px 20px'}; |
|
|
z-index: 10000; |
|
|
display: none; |
|
|
flex-direction: column; |
|
|
gap: ${isTouch ? '12px' : '15px'}; |
|
|
overflow-y: auto; |
|
|
user-select: none; |
|
|
border-left: ${isTouch ? 'none' : '2px solid #ff00ff'}; |
|
|
box-shadow: -10px 0 30px rgba(0,0,0,0.8); |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
/* Touch toggle button */ |
|
|
#cheat-toggle-btn { |
|
|
display: ${isTouch ? 'flex' : 'none'}; |
|
|
position: fixed; |
|
|
top: 10px; |
|
|
right: 10px; |
|
|
width: 50px; |
|
|
height: 50px; |
|
|
background: rgba(255, 0, 255, 0.3); |
|
|
border: 2px solid #ff00ff; |
|
|
border-radius: 50%; |
|
|
color: #ff00ff; |
|
|
font-size: 20px; |
|
|
font-weight: bold; |
|
|
z-index: 9999; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
cursor: pointer; |
|
|
backdrop-filter: blur(5px); |
|
|
touch-action: manipulation; |
|
|
} |
|
|
#cheat-toggle-btn:active { |
|
|
background: rgba(255, 0, 255, 0.6); |
|
|
transform: scale(0.95); |
|
|
} |
|
|
#cheat-toggle-btn.active { |
|
|
background: rgba(255, 0, 255, 0.6); |
|
|
} |
|
|
|
|
|
/* Airbreak touch controls */ |
|
|
#airbreak-touch-controls { |
|
|
display: none; |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
left: 20px; |
|
|
z-index: 10001; |
|
|
touch-action: none; |
|
|
} |
|
|
#airbreak-touch-controls.active { |
|
|
display: block; |
|
|
} |
|
|
.airbreak-joystick { |
|
|
width: 120px; |
|
|
height: 120px; |
|
|
background: rgba(0, 255, 255, 0.2); |
|
|
border: 2px solid #0ff; |
|
|
border-radius: 50%; |
|
|
position: relative; |
|
|
touch-action: none; |
|
|
} |
|
|
.airbreak-joystick-knob { |
|
|
width: 50px; |
|
|
height: 50px; |
|
|
background: rgba(0, 255, 255, 0.6); |
|
|
border: 2px solid #0ff; |
|
|
border-radius: 50%; |
|
|
position: absolute; |
|
|
top: 50%; |
|
|
left: 50%; |
|
|
transform: translate(-50%, -50%); |
|
|
pointer-events: none; |
|
|
} |
|
|
.airbreak-vertical-btns { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 10px; |
|
|
z-index: 10001; |
|
|
} |
|
|
.airbreak-v-btn { |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
background: rgba(0, 255, 255, 0.2); |
|
|
border: 2px solid #0ff; |
|
|
border-radius: 10px; |
|
|
color: #0ff; |
|
|
font-size: 24px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
touch-action: manipulation; |
|
|
} |
|
|
.airbreak-v-btn:active { |
|
|
background: rgba(0, 255, 255, 0.5); |
|
|
} |
|
|
#airbreak-toggle-fly { |
|
|
position: fixed; |
|
|
bottom: 160px; |
|
|
right: 20px; |
|
|
width: 80px; |
|
|
height: 40px; |
|
|
background: rgba(255, 255, 0, 0.2); |
|
|
border: 2px solid #ff0; |
|
|
border-radius: 10px; |
|
|
color: #ff0; |
|
|
font-size: 12px; |
|
|
font-weight: bold; |
|
|
z-index: 10001; |
|
|
display: none; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
touch-action: manipulation; |
|
|
} |
|
|
#airbreak-toggle-fly.active { |
|
|
background: rgba(255, 255, 0, 0.5); |
|
|
} |
|
|
#airbreak-toggle-fly.visible { |
|
|
display: flex; |
|
|
} |
|
|
#cheat-engine-ui h1 { |
|
|
font-size: 18px; |
|
|
text-align: left; |
|
|
margin: 0 0 10px 0; |
|
|
color: #ff00ff; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 1px; |
|
|
border-bottom: 1px solid #333; |
|
|
padding-bottom: 10px; |
|
|
} |
|
|
.ce-section { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 8px; |
|
|
} |
|
|
.ce-label { |
|
|
font-size: 10px; |
|
|
color: #888; |
|
|
text-transform: uppercase; |
|
|
} |
|
|
#cheat-engine-ui input, #cheat-engine-ui select, #cheat-engine-ui button { |
|
|
background: rgba(255, 255, 255, 0.05); |
|
|
color: #fff; |
|
|
border: 1px solid #333; |
|
|
padding: 6px 10px; |
|
|
font-size: 12px; |
|
|
outline: none; |
|
|
} |
|
|
#cheat-engine-ui input:focus { |
|
|
border-color: #ff00ff; |
|
|
} |
|
|
#cheat-engine-ui button { |
|
|
cursor: pointer; |
|
|
transition: all 0.1s; |
|
|
text-transform: uppercase; |
|
|
font-size: ${isTouch ? '12px' : '10px'}; |
|
|
min-height: ${isTouch ? '44px' : 'auto'}; |
|
|
touch-action: manipulation; |
|
|
} |
|
|
#cheat-engine-ui button:hover, #cheat-engine-ui button:active { |
|
|
background: #ff00ff; |
|
|
color: #000; |
|
|
border-color: #ff00ff; |
|
|
} |
|
|
#cheat-engine-ui .results { |
|
|
max-height: ${isTouch ? '150px' : '120px'}; |
|
|
overflow-y: auto; |
|
|
background: rgba(0,0,0,0.3); |
|
|
border: 1px solid #222; |
|
|
} |
|
|
.cheat-grid { |
|
|
display: grid; |
|
|
grid-template-columns: 1fr 1fr; |
|
|
gap: ${isTouch ? '8px' : '4px'}; |
|
|
} |
|
|
.cheat-cat { |
|
|
font-size: ${isTouch ? '13px' : '11px'}; |
|
|
color: #0ff; |
|
|
margin-top: ${isTouch ? '15px' : '10px'}; |
|
|
text-transform: uppercase; |
|
|
} |
|
|
.cheat-btn { |
|
|
background: transparent !important; |
|
|
border: 1px solid #444 !important; |
|
|
text-align: left; |
|
|
padding: ${isTouch ? '12px 10px' : '6px 10px'}; |
|
|
} |
|
|
.cheat-btn:hover, .cheat-btn:active { |
|
|
border-color: #0ff !important; |
|
|
color: #0ff !important; |
|
|
background: rgba(0, 255, 255, 0.1) !important; |
|
|
} |
|
|
|
|
|
/* Close button for touch */ |
|
|
#cheat-close-btn { |
|
|
display: ${isTouch ? 'flex' : 'none'}; |
|
|
position: absolute; |
|
|
top: 15px; |
|
|
right: 15px; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
background: rgba(255, 0, 0, 0.3); |
|
|
border: 2px solid #f00; |
|
|
border-radius: 50%; |
|
|
color: #f00; |
|
|
font-size: 20px; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
cursor: pointer; |
|
|
touch-action: manipulation; |
|
|
} |
|
|
#cheat-close-btn:active { |
|
|
background: rgba(255, 0, 0, 0.6); |
|
|
} |
|
|
::-webkit-scrollbar { |
|
|
width: 4px; |
|
|
} |
|
|
::-webkit-scrollbar-thumb { |
|
|
background: #333; |
|
|
} |
|
|
::-webkit-scrollbar-thumb:hover { |
|
|
background: #ff00ff; |
|
|
} |
|
|
`; |
|
|
document.head.appendChild(style); |
|
|
|
|
|
|
|
|
const toggleBtn = document.createElement('div'); |
|
|
toggleBtn.id = 'cheat-toggle-btn'; |
|
|
toggleBtn.innerHTML = 'β'; |
|
|
toggleBtn.title = 'Open Cheat Menu'; |
|
|
document.body.appendChild(toggleBtn); |
|
|
|
|
|
|
|
|
const airbreakTouchControls = document.createElement('div'); |
|
|
airbreakTouchControls.id = 'airbreak-touch-controls'; |
|
|
airbreakTouchControls.innerHTML = ` |
|
|
<div class="airbreak-joystick" id="airbreak-joystick"> |
|
|
<div class="airbreak-joystick-knob" id="airbreak-joystick-knob"></div> |
|
|
</div> |
|
|
`; |
|
|
document.body.appendChild(airbreakTouchControls); |
|
|
|
|
|
|
|
|
const airbreakVerticalBtns = document.createElement('div'); |
|
|
airbreakVerticalBtns.className = 'airbreak-vertical-btns'; |
|
|
airbreakVerticalBtns.id = 'airbreak-vertical-btns'; |
|
|
airbreakVerticalBtns.innerHTML = ` |
|
|
<div class="airbreak-v-btn" id="airbreak-up-btn">β</div> |
|
|
<div class="airbreak-v-btn" id="airbreak-down-btn">β</div> |
|
|
`; |
|
|
airbreakVerticalBtns.style.display = 'none'; |
|
|
document.body.appendChild(airbreakVerticalBtns); |
|
|
|
|
|
|
|
|
const flyToggleBtn = document.createElement('div'); |
|
|
flyToggleBtn.id = 'airbreak-toggle-fly'; |
|
|
flyToggleBtn.innerHTML = 'FLY'; |
|
|
document.body.appendChild(flyToggleBtn); |
|
|
|
|
|
const ui = document.createElement('div'); |
|
|
ui.id = 'cheat-engine-ui'; |
|
|
ui.innerHTML = ` |
|
|
<div id="cheat-close-btn">β</div> |
|
|
<h1>Cheat Engine</h1> |
|
|
|
|
|
<div class="ce-section"> |
|
|
<div class="ce-label">Scanner</div> |
|
|
<div style="display: flex; gap: 4px;"> |
|
|
<input type="text" id="ce-value" placeholder="Value" style="flex: 1;"> |
|
|
<select id="ce-type"> |
|
|
<option value="any">Any</option> |
|
|
<option value="i32">i32</option> |
|
|
<option value="f32">f32</option> |
|
|
<option value="i16">i16</option> |
|
|
<option value="i8">i8</option> |
|
|
</select> |
|
|
</div> |
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 4px;"> |
|
|
<button id="ce-search">New</button> |
|
|
<button id="ce-next">Next</button> |
|
|
<button id="ce-reset">Reset</button> |
|
|
</div> |
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 4px;"> |
|
|
<button id="ce-snap" title="Capture current values">Snap</button> |
|
|
<button id="ce-inc" title="Find increased values">Inc</button> |
|
|
<button id="ce-dec" title="Find decreased values">Dec</button> |
|
|
<button id="ce-changed" title="Find changed values">Chg</button> |
|
|
</div> |
|
|
<div id="ce-status" style="font-size: 10px; color: #888;">Ready</div> |
|
|
<div class="results" id="ce-results"></div> |
|
|
</div> |
|
|
|
|
|
<div class="ce-section"> |
|
|
<div class="ce-label">Manual</div> |
|
|
<div style="display: flex; gap: 4px;"> |
|
|
<input type="text" id="ce-manual-addr" placeholder="0xAddress" style="flex: 1;"> |
|
|
<button id="ce-view-addr">View</button> |
|
|
</div> |
|
|
<div id="ce-manual-results" style="font-size: 10px; display: none;"></div> |
|
|
</div> |
|
|
|
|
|
<div class="ce-section"> |
|
|
<div class="ce-label">AirBreak (RShift toggle)</div> |
|
|
<div style="display: flex; gap: 4px;"> |
|
|
<input type="text" id="ce-health-addr" placeholder="Health addr (0x...)" style="flex: 1;"> |
|
|
<button id="ce-setup-airbreak">Setup</button> |
|
|
</div> |
|
|
<div style="display: flex; gap: 4px; margin-top: 4px;"> |
|
|
<span style="font-size: 10px; color: #888;">Speed:</span> |
|
|
<input type="number" id="ce-fly-speed" value="2.0" step="0.5" min="0.1" max="50" style="width: 60px;"> |
|
|
</div> |
|
|
<div id="ce-airbreak-status" style="font-size: 10px; color: #888;">Not configured</div> |
|
|
<div id="ce-pos-display" style="font-size: 9px; color: #666;"></div> |
|
|
</div> |
|
|
|
|
|
<div class="ce-section"> |
|
|
<div class="ce-label">Cheats</div> |
|
|
|
|
|
<div class="cheat-cat">Weapons & Health</div> |
|
|
<div class="cheat-grid"> |
|
|
<button class="cheat-btn" onclick="typeCheat('THUGSTOOLS')">Thug Tools</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('PROFESSIONALTOOLS')">Pro Tools</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('NUTTERTOOLS')">Nutter Tools</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ASPIRINE')">Health</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('PRECIOUSPROTECTION')">Armor</button> |
|
|
</div> |
|
|
|
|
|
<div class="cheat-cat">Gameplay</div> |
|
|
<div class="cheat-grid"> |
|
|
<button class="cheat-btn" onclick="typeCheat('LEAVEMEALONE')">No Wanted</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('YOUWONTTAKEMEALIVE')">Wanted +2</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ONSPEED')">Fast Game</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('BOOOOOORING')">Slow Game</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('LIFEISPASSINGMEBY')">Fast Time</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('BIGBANG')">Explode All</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('FIGHTFIGHTFIGHT')">Peds Riot</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('NOBODYLIKESME')">Peds Attack</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('OURGODGIVENRIGHTTOBEARARMS')">Peds Armed</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('CHICKSWITHGUNS')">Armed Girls</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('FANNYMAGNET')">Ladies Man</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('HOPINGIRL')">Get in Car</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('GREENLIGHT')">Green Lights</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('MIAMITRAFFIC')">Fast Traffic</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ICANTTAKEITANYMORE')">Suicide</button> |
|
|
</div> |
|
|
|
|
|
<div class="cheat-cat">Skins</div> |
|
|
<div class="cheat-grid"> |
|
|
<button class="cheat-btn" onclick="typeCheat('STILLLIKEDRESSINGUP')">Random Skin</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('IDONTHAVETHEMONEYSONNY')">Sonny</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('LOOKLIKELANCE')">Lance</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ILOOKLIKEHILARY')">Hilary</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ROCKANDROLLMAN')">Jezz</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('WELOVEOURDICK')">Dick</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('MYSONISALAWYER')">Ken</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ONEARMEDBANDIT')">Phil</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('FOXYLITTLETHING')">Mercedes</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('CHEATSHAVEBEENCRACKED')">Diaz</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('IWANTBIGTITS')">Candy</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('CERTAINDEATH')">Cigarette</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('DEEPFRIEDMARSBARS')">Fat Tommy</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('PROGRAMMER')">Thin Tommy</button> |
|
|
</div> |
|
|
|
|
|
<div class="cheat-cat">Vehicles</div> |
|
|
<div class="cheat-grid"> |
|
|
<button class="cheat-btn" onclick="typeCheat('PANZER')">Tank</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('GETTHEREFAST')">Sabre Turbo</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('GETTHEREQUICKLY')">Bloodring A</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('TRAVELINSTYLE')">Bloodring B</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('GETTHEREVERYFASTINDEED')">Hotring A</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('GETTHEREAMAZINGLYFAST')">Hotring B</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('THELASTRIDE')">Hearse</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ROCKANDROLLCAR')">Limo</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('BETTERTHANWALKING')">Caddy</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('RUBBISHCAR')">Trashmaster</button> |
|
|
</div> |
|
|
|
|
|
<div class="cheat-cat">Vehicle Effects</div> |
|
|
<div class="cheat-grid"> |
|
|
<button class="cheat-btn" onclick="typeCheat('COMEFLYWITHME')">Flying Cars</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('AIRSHIP')">Flying Boats</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('SEAWAYS')">Water Drive</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('WHEELSAREALLINEED')">Only Wheels</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('GRIPISEVERYTHING')">Perfect Handling</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('IWANTITPAINTEDBLACK')">Black Cars</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('AHAIRDRESSERSCAR')">Pink Cars</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('LOADSOFLITTLETHINGS')">Small Wheels</button> |
|
|
</div> |
|
|
|
|
|
<div class="cheat-cat">Weather</div> |
|
|
<div class="cheat-grid"> |
|
|
<button class="cheat-btn" onclick="typeCheat('ALOVELYDAY')">Sunny</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('APLEASANTDAY')">Cloudy</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('ABITDRIEG')">Very Cloudy</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('CATSANDDOGS')">Rainy</button> |
|
|
<button class="cheat-btn" onclick="typeCheat('CANTSEEATHING')">Foggy</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
document.body.appendChild(ui); |
|
|
|
|
|
let results = []; |
|
|
let isSearching = false; |
|
|
let menuOpen = false; |
|
|
let lastBuffer = null; |
|
|
|
|
|
|
|
|
let airbreakEnabled = false; |
|
|
let airbreakConfigured = false; |
|
|
let playerMatrixAddr = 0; |
|
|
let flySpeed = 2.0; |
|
|
let keysPressed = { w: false, s: false, a: false, d: false, space: false, shift: false }; |
|
|
|
|
|
|
|
|
function getView() { |
|
|
const buf = HEAPU8.buffer; |
|
|
if (lastBuffer !== buf) { |
|
|
lastBuffer = buf; |
|
|
console.log('[CheatEngine] Buffer changed, size:', buf.byteLength); |
|
|
} |
|
|
return new DataView(buf); |
|
|
} |
|
|
|
|
|
|
|
|
function readValue(view, addr, type) { |
|
|
try { |
|
|
const bufLen = view.buffer.byteLength; |
|
|
if (addr < 0 || addr >= bufLen - 8) return null; |
|
|
switch(type) { |
|
|
case 'i32': return view.getInt32(addr, true); |
|
|
case 'f32': return view.getFloat32(addr, true); |
|
|
case 'i16': return view.getInt16(addr, true); |
|
|
case 'i8': return view.getInt8(addr); |
|
|
case 'f64': return view.getFloat64(addr, true); |
|
|
} |
|
|
} catch(e) { |
|
|
console.warn('[CheatEngine] Read error at', addr, e); |
|
|
} |
|
|
return null; |
|
|
} |
|
|
|
|
|
|
|
|
function snapshotValues() { |
|
|
if (results.length === 0) return; |
|
|
const view = getView(); |
|
|
let updated = 0; |
|
|
for (const res of results) { |
|
|
const val = readValue(view, res.addr, res.type); |
|
|
if (val !== null) { |
|
|
res.lastVal = val; |
|
|
updated++; |
|
|
} |
|
|
} |
|
|
updateStatus(`Snapshot: ${updated} values captured`); |
|
|
} |
|
|
|
|
|
|
|
|
const originalRegister = JSEvents.registerOrRemoveHandler; |
|
|
JSEvents.registerOrRemoveHandler = function(h) { |
|
|
if (h.handlerFunc && !h._wrapped) { |
|
|
const originalHandler = h.handlerFunc; |
|
|
h.handlerFunc = function(e) { |
|
|
if (menuOpen && !e._isCheat && (h.eventTypeString.startsWith('key') || h.eventTypeString.startsWith('mouse') || h.eventTypeString.startsWith('touch'))) { |
|
|
if (e && e.key === 'F3') return originalHandler.apply(this, arguments); |
|
|
return; |
|
|
} |
|
|
return originalHandler.apply(this, arguments); |
|
|
}; |
|
|
h._wrapped = true; |
|
|
} |
|
|
return originalRegister.apply(this, arguments); |
|
|
}; |
|
|
|
|
|
if (JSEvents.eventHandlers) { |
|
|
JSEvents.eventHandlers.forEach(h => { |
|
|
if (h.handlerFunc && !h._wrapped) { |
|
|
const originalHandler = h.handlerFunc; |
|
|
h.handlerFunc = function(e) { |
|
|
if (menuOpen && !e._isCheat && (h.eventTypeString.startsWith('key') || h.eventTypeString.startsWith('mouse') || h.eventTypeString.startsWith('touch'))) { |
|
|
if (e && e.key === 'F3') return originalHandler.apply(this, arguments); |
|
|
return; |
|
|
} |
|
|
return originalHandler.apply(this, arguments); |
|
|
}; |
|
|
h._wrapped = true; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
function toggleMenu() { |
|
|
menuOpen = !menuOpen; |
|
|
ui.style.display = menuOpen ? 'flex' : 'none'; |
|
|
toggleBtn.classList.toggle('active', menuOpen); |
|
|
|
|
|
if (menuOpen) { |
|
|
if (document.pointerLockElement) document.exitPointerLock(); |
|
|
document.body.style.cursor = 'default'; |
|
|
if (Module.canvas) Module.canvas.style.cursor = 'default'; |
|
|
} else { |
|
|
if (Module.canvas) Module.canvas.style.cursor = 'none'; |
|
|
document.body.style.cursor = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'F3') { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
toggleMenu(); |
|
|
} |
|
|
}, true); |
|
|
|
|
|
|
|
|
let toggleBtnDragging = false; |
|
|
let toggleBtnStartX = 0; |
|
|
let toggleBtnStartY = 0; |
|
|
let toggleBtnInitialLeft = 0; |
|
|
let toggleBtnInitialTop = 0; |
|
|
const dragThreshold = 10; |
|
|
|
|
|
|
|
|
const savedPos = localStorage.getItem('cheat-toggle-pos'); |
|
|
if (savedPos) { |
|
|
try { |
|
|
const pos = JSON.parse(savedPos); |
|
|
toggleBtn.style.right = 'auto'; |
|
|
toggleBtn.style.left = pos.left + 'px'; |
|
|
toggleBtn.style.top = pos.top + 'px'; |
|
|
} catch(e) {} |
|
|
} |
|
|
|
|
|
toggleBtn.addEventListener('click', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
if (!toggleBtnDragging) { |
|
|
toggleMenu(); |
|
|
} |
|
|
}); |
|
|
|
|
|
if (isTouch) { |
|
|
toggleBtn.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
toggleBtnDragging = false; |
|
|
|
|
|
const touch = e.touches[0]; |
|
|
toggleBtnStartX = touch.clientX; |
|
|
toggleBtnStartY = touch.clientY; |
|
|
|
|
|
|
|
|
const rect = toggleBtn.getBoundingClientRect(); |
|
|
toggleBtnInitialLeft = rect.left; |
|
|
toggleBtnInitialTop = rect.top; |
|
|
}, { passive: false }); |
|
|
|
|
|
toggleBtn.addEventListener('touchmove', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
|
|
|
const touch = e.touches[0]; |
|
|
const deltaX = touch.clientX - toggleBtnStartX; |
|
|
const deltaY = touch.clientY - toggleBtnStartY; |
|
|
|
|
|
|
|
|
if (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { |
|
|
toggleBtnDragging = true; |
|
|
} |
|
|
|
|
|
if (toggleBtnDragging) { |
|
|
let newLeft = toggleBtnInitialLeft + deltaX; |
|
|
let newTop = toggleBtnInitialTop + deltaY; |
|
|
|
|
|
|
|
|
const btnWidth = toggleBtn.offsetWidth; |
|
|
const btnHeight = toggleBtn.offsetHeight; |
|
|
newLeft = Math.max(0, Math.min(window.innerWidth - btnWidth, newLeft)); |
|
|
newTop = Math.max(0, Math.min(window.innerHeight - btnHeight, newTop)); |
|
|
|
|
|
toggleBtn.style.right = 'auto'; |
|
|
toggleBtn.style.left = newLeft + 'px'; |
|
|
toggleBtn.style.top = newTop + 'px'; |
|
|
} |
|
|
}, { passive: false }); |
|
|
|
|
|
toggleBtn.addEventListener('touchend', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
|
|
|
if (toggleBtnDragging) { |
|
|
|
|
|
const rect = toggleBtn.getBoundingClientRect(); |
|
|
localStorage.setItem('cheat-toggle-pos', JSON.stringify({ |
|
|
left: rect.left, |
|
|
top: rect.top |
|
|
})); |
|
|
} else { |
|
|
|
|
|
toggleMenu(); |
|
|
} |
|
|
|
|
|
toggleBtnDragging = false; |
|
|
}, { passive: false }); |
|
|
} |
|
|
|
|
|
|
|
|
const closeBtn = document.getElementById('cheat-close-btn'); |
|
|
if (closeBtn) { |
|
|
closeBtn.addEventListener('click', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
toggleMenu(); |
|
|
}); |
|
|
|
|
|
if (isTouch) { |
|
|
closeBtn.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
toggleMenu(); |
|
|
}, { passive: false }); |
|
|
} |
|
|
} |
|
|
|
|
|
window.addEventListener('keydown', (e) => { |
|
|
if (menuOpen && e.target.tagName === 'INPUT') e.stopPropagation(); |
|
|
}, true); |
|
|
window.addEventListener('keyup', (e) => { |
|
|
if (menuOpen && e.target.tagName === 'INPUT') e.stopPropagation(); |
|
|
}, true); |
|
|
window.addEventListener('keypress', (e) => { |
|
|
if (menuOpen && e.target.tagName === 'INPUT') e.stopPropagation(); |
|
|
}, true); |
|
|
window.addEventListener('mousedown', (e) => { |
|
|
if (menuOpen && ui.contains(e.target)) e.stopPropagation(); |
|
|
}, true); |
|
|
|
|
|
|
|
|
window.addEventListener('touchstart', (e) => { |
|
|
if (menuOpen && ui.contains(e.target)) e.stopPropagation(); |
|
|
}, true); |
|
|
window.addEventListener('touchmove', (e) => { |
|
|
if (menuOpen && ui.contains(e.target)) e.stopPropagation(); |
|
|
}, true); |
|
|
window.addEventListener('touchend', (e) => { |
|
|
if (menuOpen && ui.contains(e.target)) e.stopPropagation(); |
|
|
}, true); |
|
|
|
|
|
function updateStatus(text) { |
|
|
document.getElementById('ce-status').textContent = text; |
|
|
} |
|
|
|
|
|
function checkMatch(view, addr, type, val, tolerance = 0.5) { |
|
|
try { |
|
|
switch(type) { |
|
|
case 'i32': { |
|
|
const v = view.getInt32(addr, true); |
|
|
return Math.abs(v - val) <= tolerance; |
|
|
} |
|
|
case 'f32': { |
|
|
const v = view.getFloat32(addr, true); |
|
|
|
|
|
if (!isFinite(v)) return false; |
|
|
return Math.abs(v - val) <= tolerance; |
|
|
} |
|
|
case 'i16': { |
|
|
const v = view.getInt16(addr, true); |
|
|
return Math.abs(v - val) <= tolerance; |
|
|
} |
|
|
case 'i8': { |
|
|
const v = view.getInt8(addr); |
|
|
return Math.abs(v - val) <= tolerance; |
|
|
} |
|
|
case 'f64': { |
|
|
const v = view.getFloat64(addr, true); |
|
|
if (!isFinite(v)) return false; |
|
|
return Math.abs(v - val) <= tolerance; |
|
|
} |
|
|
case 'any': |
|
|
|
|
|
const i32 = view.getInt32(addr, true); |
|
|
if (Math.abs(i32 - val) <= tolerance) return 'i32'; |
|
|
|
|
|
const f32 = view.getFloat32(addr, true); |
|
|
if (isFinite(f32) && Math.abs(f32 - val) <= tolerance) return 'f32'; |
|
|
|
|
|
const i16 = view.getInt16(addr, true); |
|
|
if (Math.abs(i16 - val) <= tolerance) return 'i16'; |
|
|
|
|
|
const i8 = view.getInt8(addr); |
|
|
if (Math.abs(i8 - val) <= tolerance) return 'i8'; |
|
|
|
|
|
return false; |
|
|
} |
|
|
} catch(e) {} |
|
|
return false; |
|
|
} |
|
|
|
|
|
function firstSearch() { |
|
|
if (isSearching) return; |
|
|
const valStr = document.getElementById('ce-value').value; |
|
|
const type = document.getElementById('ce-type').value; |
|
|
const val = parseFloat(valStr); |
|
|
|
|
|
if (isNaN(val)) { |
|
|
updateStatus("Invalid value"); |
|
|
return; |
|
|
} |
|
|
|
|
|
isSearching = true; |
|
|
updateStatus("Searching..."); |
|
|
results = []; |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
const view = getView(); |
|
|
const bufferLen = view.buffer.byteLength; |
|
|
|
|
|
console.log(`[CheatEngine] New search: value=${val}, type=${type}, bufferLen=${bufferLen}`); |
|
|
|
|
|
|
|
|
const step = (type === 'i8') ? 1 : 4; |
|
|
|
|
|
for (let i = 0; i < bufferLen - 8; i += step) { |
|
|
const matchType = checkMatch(view, i, type, val); |
|
|
if (matchType) { |
|
|
const resType = type === 'any' ? matchType : type; |
|
|
const currentVal = readValue(view, i, resType); |
|
|
if (currentVal !== null) { |
|
|
results.push({addr: i, type: resType, lastVal: currentVal}); |
|
|
if (results.length > 100000) break; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(`[CheatEngine] Search complete: found ${results.length} addresses`); |
|
|
updateStatus(`Found ${results.length} addresses`); |
|
|
displayResults(); |
|
|
isSearching = false; |
|
|
}, 10); |
|
|
} |
|
|
|
|
|
function nextSearch() { |
|
|
if (isSearching || results.length === 0) { |
|
|
if (results.length === 0) { |
|
|
updateStatus("No results. Do 'New' search first."); |
|
|
} |
|
|
return; |
|
|
} |
|
|
const valStr = document.getElementById('ce-value').value; |
|
|
const val = parseFloat(valStr); |
|
|
|
|
|
if (isNaN(val)) { |
|
|
updateStatus("Invalid value"); |
|
|
return; |
|
|
} |
|
|
|
|
|
isSearching = true; |
|
|
const startCount = results.length; |
|
|
updateStatus(`Filtering ${startCount} addresses...`); |
|
|
const newResults = []; |
|
|
const view = getView(); |
|
|
const bufLen = view.buffer.byteLength; |
|
|
|
|
|
console.log(`[CheatEngine] Next search: value=${val}, checking ${results.length} addresses`); |
|
|
|
|
|
let skipped = 0; |
|
|
let checked = 0; |
|
|
|
|
|
for (const res of results) { |
|
|
|
|
|
if (res.addr < 0 || res.addr >= bufLen - 8) { |
|
|
skipped++; |
|
|
continue; |
|
|
} |
|
|
|
|
|
checked++; |
|
|
const currentVal = readValue(view, res.addr, res.type); |
|
|
|
|
|
if (currentVal === null) { |
|
|
skipped++; |
|
|
continue; |
|
|
} |
|
|
|
|
|
|
|
|
const tolerance = 0.5; |
|
|
const matches = Math.abs(currentVal - val) <= tolerance; |
|
|
|
|
|
if (matches) { |
|
|
res.lastVal = currentVal; |
|
|
newResults.push(res); |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(`[CheatEngine] Checked: ${checked}, Skipped: ${skipped}, Found: ${newResults.length}`); |
|
|
|
|
|
results = newResults; |
|
|
updateStatus(`Found ${results.length}/${startCount} addresses`); |
|
|
displayResults(); |
|
|
isSearching = false; |
|
|
} |
|
|
|
|
|
function displayResults() { |
|
|
const container = document.getElementById('ce-results'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
const limit = Math.min(results.length, 100); |
|
|
const view = getView(); |
|
|
for (let i = 0; i < limit; i++) { |
|
|
const res = results[i]; |
|
|
const div = document.createElement('div'); |
|
|
div.style.display = 'flex'; |
|
|
div.style.justifyContent = 'space-between'; |
|
|
div.style.marginBottom = '2px'; |
|
|
div.style.borderBottom = '1px solid #333'; |
|
|
|
|
|
let currentVal; |
|
|
try { |
|
|
switch(res.type) { |
|
|
case 'i32': currentVal = view.getInt32(res.addr, true); break; |
|
|
case 'f32': currentVal = view.getFloat32(res.addr, true).toFixed(2); break; |
|
|
case 'i16': currentVal = view.getInt16(res.addr, true); break; |
|
|
case 'i8': currentVal = view.getInt8(res.addr); break; |
|
|
case 'f64': currentVal = view.getFloat64(res.addr, true).toFixed(2); break; |
|
|
} |
|
|
} catch(e) { currentVal = "???"; } |
|
|
|
|
|
div.innerHTML = ` |
|
|
<span style="color: #ff00ff;">0x${res.addr.toString(16)}</span> |
|
|
<span style="color: #0ff;">${currentVal}</span> |
|
|
<button class="cheat-btn" onclick="editAddr(${res.addr}, '${res.type}')">Edit</button> |
|
|
`; |
|
|
container.appendChild(div); |
|
|
} |
|
|
} |
|
|
|
|
|
window.editAddr = function(addr, type) { |
|
|
const newVal = prompt(`Enter new value for 0x${addr.toString(16)} (${type}):`); |
|
|
if (newVal === null) return; |
|
|
|
|
|
const view = getView(); |
|
|
try { |
|
|
switch(type) { |
|
|
case 'i32': view.setInt32(addr, parseInt(newVal), true); break; |
|
|
case 'f32': view.setFloat32(addr, parseFloat(newVal), true); break; |
|
|
case 'i16': view.setInt16(addr, parseInt(newVal), true); break; |
|
|
case 'i8': view.setInt8(addr, parseInt(newVal)); break; |
|
|
case 'f64': view.setFloat64(addr, parseFloat(newVal), true); break; |
|
|
} |
|
|
} catch(e) { alert("Error writing to memory"); } |
|
|
displayResults(); |
|
|
if (document.getElementById('ce-manual-results').style.display === 'block') { |
|
|
viewManualAddr(); |
|
|
} |
|
|
}; |
|
|
|
|
|
function filterResults(mode) { |
|
|
if (isSearching || results.length === 0) { |
|
|
if (results.length === 0) { |
|
|
updateStatus("No results to filter. Do 'New' search first."); |
|
|
} |
|
|
return; |
|
|
} |
|
|
isSearching = true; |
|
|
updateStatus("Filtering..."); |
|
|
|
|
|
const newResults = []; |
|
|
const view = getView(); |
|
|
const bufLen = view.buffer.byteLength; |
|
|
|
|
|
for (const res of results) { |
|
|
|
|
|
if (res.addr < 0 || res.addr >= bufLen - 8) continue; |
|
|
|
|
|
const oldVal = res.lastVal; |
|
|
const newVal = readValue(view, res.addr, res.type); |
|
|
|
|
|
if (newVal === null) continue; |
|
|
|
|
|
|
|
|
if (oldVal === undefined || oldVal === null) { |
|
|
|
|
|
res.lastVal = newVal; |
|
|
continue; |
|
|
} |
|
|
|
|
|
let match = false; |
|
|
if (mode === 'inc' && newVal > oldVal) match = true; |
|
|
if (mode === 'dec' && newVal < oldVal) match = true; |
|
|
if (mode === 'changed' && Math.abs(newVal - oldVal) > 0.0001) match = true; |
|
|
|
|
|
if (match) { |
|
|
res.lastVal = newVal; |
|
|
newResults.push(res); |
|
|
} |
|
|
} |
|
|
|
|
|
results = newResults; |
|
|
updateStatus(`Found ${results.length} addresses (compared to snapshot)`); |
|
|
displayResults(); |
|
|
isSearching = false; |
|
|
} |
|
|
|
|
|
function viewManualAddr() { |
|
|
const addrStr = document.getElementById('ce-manual-addr').value; |
|
|
const addr = parseInt(addrStr, 16); |
|
|
const view = getView(); |
|
|
const bufLen = view.buffer.byteLength; |
|
|
|
|
|
if (isNaN(addr) || addr < 0 || addr >= bufLen - 8) { |
|
|
alert("Invalid address (out of range: 0 - 0x" + (bufLen - 8).toString(16) + ")"); |
|
|
return; |
|
|
} |
|
|
|
|
|
const container = document.getElementById('ce-manual-results'); |
|
|
container.style.display = 'block'; |
|
|
container.innerHTML = ''; |
|
|
const types = ['i8', 'i16', 'i32', 'f32', 'f64']; |
|
|
|
|
|
types.forEach(type => { |
|
|
let val; |
|
|
try { |
|
|
switch(type) { |
|
|
case 'i8': val = view.getInt8(addr); break; |
|
|
case 'i16': val = view.getInt16(addr, true); break; |
|
|
case 'i32': val = view.getInt32(addr, true); break; |
|
|
case 'f32': val = view.getFloat32(addr, true).toFixed(4); break; |
|
|
case 'f64': val = view.getFloat64(addr, true).toFixed(4); break; |
|
|
} |
|
|
|
|
|
const div = document.createElement('div'); |
|
|
div.style.display = 'flex'; |
|
|
div.style.justifyContent = 'space-between'; |
|
|
div.style.marginBottom = '2px'; |
|
|
div.innerHTML = ` |
|
|
<span style="color: #0ff;">${type.toUpperCase()}:</span> |
|
|
<span>${val}</span> |
|
|
<button class="cheat-btn" onclick="editAddr(${addr}, '${type}')">Edit</button> |
|
|
`; |
|
|
container.appendChild(div); |
|
|
} catch(e) {} |
|
|
}); |
|
|
} |
|
|
|
|
|
document.getElementById('ce-view-addr').onclick = viewManualAddr; |
|
|
document.getElementById('ce-search').onclick = firstSearch; |
|
|
document.getElementById('ce-next').onclick = nextSearch; |
|
|
document.getElementById('ce-snap').onclick = snapshotValues; |
|
|
document.getElementById('ce-inc').onclick = () => filterResults('inc'); |
|
|
document.getElementById('ce-dec').onclick = () => filterResults('dec'); |
|
|
document.getElementById('ce-changed').onclick = () => filterResults('changed'); |
|
|
document.getElementById('ce-reset').onclick = () => { |
|
|
results = []; |
|
|
document.getElementById('ce-results').innerHTML = ''; |
|
|
updateStatus("Reset"); |
|
|
}; |
|
|
|
|
|
window.typeCheat = async function(code) { |
|
|
console.log("Typing cheat:", code); |
|
|
updateStatus("Entering cheat..."); |
|
|
|
|
|
if (typeof JSEvents === 'undefined' || !JSEvents.eventHandlers) return; |
|
|
|
|
|
const handlers = JSEvents.eventHandlers.filter(h => |
|
|
h.eventTypeString === 'keydown' || h.eventTypeString === 'keypress' || h.eventTypeString === 'keyup' |
|
|
); |
|
|
|
|
|
const eventDataPtr = _malloc(160); |
|
|
|
|
|
for (let i = 0; i < code.length; i++) { |
|
|
const char = code[i].toUpperCase(); |
|
|
const keyCode = char.charCodeAt(0); |
|
|
|
|
|
const fillBuffer = () => { |
|
|
for (let j = 0; j < 160; j++) HEAPU8[eventDataPtr + j] = 0; |
|
|
HEAPF64[eventDataPtr >> 3] = performance.now(); |
|
|
const idx = eventDataPtr >> 2; |
|
|
HEAP32[idx + 5] = keyCode; |
|
|
HEAP32[idx + 6] = keyCode; |
|
|
HEAP32[idx + 7] = keyCode; |
|
|
stringToUTF8(char, eventDataPtr + 32, 32); |
|
|
stringToUTF8('Key' + char, eventDataPtr + 64, 32); |
|
|
stringToUTF8(char, eventDataPtr + 96, 32); |
|
|
}; |
|
|
|
|
|
const fakeEvent = { _isCheat: true }; |
|
|
|
|
|
fillBuffer(); |
|
|
for (const h of handlers) { |
|
|
if (h.eventTypeString === 'keydown') getWasmTableEntry(h.callbackfunc)(h.eventTypeId, eventDataPtr, h.userData); |
|
|
} |
|
|
fillBuffer(); |
|
|
for (const h of handlers) { |
|
|
if (h.eventTypeString === 'keypress') getWasmTableEntry(h.callbackfunc)(h.eventTypeId, eventDataPtr, h.userData); |
|
|
} |
|
|
fillBuffer(); |
|
|
for (const h of handlers) { |
|
|
if (h.eventTypeString === 'keyup') getWasmTableEntry(h.callbackfunc)(h.eventTypeId, eventDataPtr, h.userData); |
|
|
} |
|
|
|
|
|
await new Promise(r => setTimeout(r, 5)); |
|
|
} |
|
|
|
|
|
_free(eventDataPtr); |
|
|
updateStatus("Cheat entered: " + code); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let positionAddr = 0; |
|
|
let headingAddr = 0; |
|
|
let healthAddr = 0; |
|
|
let moveSpeedAddr = 0; |
|
|
let lockedZ = 0; |
|
|
let lockedHealth = 100; |
|
|
|
|
|
function setupAirbreak() { |
|
|
const healthAddrStr = document.getElementById('ce-health-addr').value; |
|
|
healthAddr = parseInt(healthAddrStr, 16); |
|
|
const view = getView(); |
|
|
const bufLen = view.buffer.byteLength; |
|
|
|
|
|
if (isNaN(healthAddr) || healthAddr < 0x400 || healthAddr >= bufLen) { |
|
|
document.getElementById('ce-airbreak-status').textContent = 'Invalid health address'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const HEALTH_OFFSET = 0x354; |
|
|
const MATRIX_OFFSET = 0x04; |
|
|
const X_IN_MATRIX = 0x34; |
|
|
const HEADING_OFFSET = 0x24; |
|
|
const MOVESPEED_OFFSET = 0x74; |
|
|
|
|
|
const entityBase = healthAddr - HEALTH_OFFSET; |
|
|
positionAddr = entityBase + MATRIX_OFFSET + X_IN_MATRIX; |
|
|
headingAddr = healthAddr + HEADING_OFFSET; |
|
|
moveSpeedAddr = entityBase + MOVESPEED_OFFSET; |
|
|
|
|
|
console.log(`[AirBreak] Health addr: 0x${healthAddr.toString(16)}`); |
|
|
console.log(`[AirBreak] Entity base: 0x${entityBase.toString(16)}`); |
|
|
console.log(`[AirBreak] Position addr: 0x${positionAddr.toString(16)}`); |
|
|
console.log(`[AirBreak] Heading addr: 0x${headingAddr.toString(16)}`); |
|
|
console.log(`[AirBreak] MoveSpeed addr: 0x${moveSpeedAddr.toString(16)}`); |
|
|
|
|
|
|
|
|
if (positionAddr < 0 || positionAddr >= bufLen - 12 || |
|
|
headingAddr < 0 || headingAddr >= bufLen - 4) { |
|
|
document.getElementById('ce-airbreak-status').textContent = 'Address out of range'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const x = view.getFloat32(positionAddr, true); |
|
|
const y = view.getFloat32(positionAddr + 4, true); |
|
|
const z = view.getFloat32(positionAddr + 8, true); |
|
|
const heading = view.getFloat32(headingAddr, true); |
|
|
const health = view.getFloat32(healthAddr, true); |
|
|
|
|
|
console.log(`[AirBreak] Position: X=${x.toFixed(2)}, Y=${y.toFixed(2)}, Z=${z.toFixed(2)}`); |
|
|
console.log(`[AirBreak] Heading: ${heading.toFixed(2)} rad (${(heading * 180 / Math.PI).toFixed(1)}Β°)`); |
|
|
console.log(`[AirBreak] Health: ${health.toFixed(2)}`); |
|
|
|
|
|
|
|
|
lockedZ = z; |
|
|
lockedHealth = health; |
|
|
|
|
|
|
|
|
if (!isFinite(x) || !isFinite(y) || !isFinite(z) || |
|
|
Math.abs(x) > 5000 || Math.abs(y) > 5000 || z < -50 || z > 1000) { |
|
|
document.getElementById('ce-airbreak-status').textContent = |
|
|
`Suspicious pos: ${x.toFixed(0)},${y.toFixed(0)},${z.toFixed(0)} - try anyway? [RShift]`; |
|
|
document.getElementById('ce-airbreak-status').style.color = '#f80'; |
|
|
} else { |
|
|
document.getElementById('ce-airbreak-status').textContent = |
|
|
`Ready! [RShift to fly] H:${(heading * 180 / Math.PI).toFixed(0)}Β°`; |
|
|
document.getElementById('ce-airbreak-status').style.color = '#0f0'; |
|
|
} |
|
|
|
|
|
|
|
|
playerMatrixAddr = positionAddr; |
|
|
airbreakConfigured = true; |
|
|
|
|
|
updatePositionDisplay(); |
|
|
} |
|
|
|
|
|
function updatePositionDisplay() { |
|
|
if (!airbreakConfigured || positionAddr === 0) return; |
|
|
|
|
|
const view = getView(); |
|
|
const bufLen = view.buffer.byteLength; |
|
|
|
|
|
if (positionAddr < 0 || positionAddr >= bufLen - 12) return; |
|
|
|
|
|
try { |
|
|
const x = view.getFloat32(positionAddr, true); |
|
|
const y = view.getFloat32(positionAddr + 4, true); |
|
|
const z = view.getFloat32(positionAddr + 8, true); |
|
|
const heading = view.getFloat32(headingAddr, true); |
|
|
const hp = view.getFloat32(healthAddr, true); |
|
|
|
|
|
document.getElementById('ce-pos-display').textContent = |
|
|
`X:${x.toFixed(1)} Y:${y.toFixed(1)} Z:${z.toFixed(1)} H:${(heading * 180 / Math.PI).toFixed(0)}Β° HP:${hp.toFixed(0)}` + |
|
|
(airbreakEnabled ? ' [FLY]' : ''); |
|
|
} catch(e) {} |
|
|
} |
|
|
|
|
|
function airbreakTick() { |
|
|
if (!airbreakEnabled || !airbreakConfigured || positionAddr === 0) return; |
|
|
|
|
|
const view = getView(); |
|
|
const bufLen = view.buffer.byteLength; |
|
|
|
|
|
if (positionAddr < 0 || positionAddr >= bufLen - 12) return; |
|
|
|
|
|
try { |
|
|
|
|
|
let x = view.getFloat32(positionAddr, true); |
|
|
let y = view.getFloat32(positionAddr + 4, true); |
|
|
let z = view.getFloat32(positionAddr + 8, true); |
|
|
|
|
|
|
|
|
const headingRaw = view.getFloat32(headingAddr, true); |
|
|
|
|
|
const heading = -headingRaw; |
|
|
|
|
|
const speed = parseFloat(document.getElementById('ce-fly-speed').value) || 2.0; |
|
|
|
|
|
|
|
|
const sinH = Math.sin(heading); |
|
|
const cosH = Math.cos(heading); |
|
|
|
|
|
|
|
|
if (keysPressed.w) { |
|
|
x += sinH * speed; |
|
|
y += cosH * speed; |
|
|
} |
|
|
if (keysPressed.s) { |
|
|
x -= sinH * speed; |
|
|
y -= cosH * speed; |
|
|
} |
|
|
if (keysPressed.a) { |
|
|
x -= cosH * speed; |
|
|
y += sinH * speed; |
|
|
} |
|
|
if (keysPressed.d) { |
|
|
x += cosH * speed; |
|
|
y -= sinH * speed; |
|
|
} |
|
|
|
|
|
|
|
|
if (keysPressed.space) { |
|
|
lockedZ += speed; |
|
|
} |
|
|
if (keysPressed.shift) { |
|
|
lockedZ -= speed; |
|
|
} |
|
|
z = lockedZ; |
|
|
|
|
|
|
|
|
view.setFloat32(positionAddr, x, true); |
|
|
view.setFloat32(positionAddr + 4, y, true); |
|
|
view.setFloat32(positionAddr + 8, z, true); |
|
|
|
|
|
|
|
|
view.setFloat32(healthAddr, lockedHealth, true); |
|
|
|
|
|
|
|
|
view.setFloat32(moveSpeedAddr, 0, true); |
|
|
view.setFloat32(moveSpeedAddr + 4, 0, true); |
|
|
view.setFloat32(moveSpeedAddr + 8, 0, true); |
|
|
|
|
|
updatePositionDisplay(); |
|
|
} catch(e) { |
|
|
console.warn('[AirBreak] Tick error:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'ShiftRight' || (e.key === 'Shift' && e.location === 2)) { |
|
|
|
|
|
if (airbreakConfigured) { |
|
|
airbreakEnabled = !airbreakEnabled; |
|
|
|
|
|
if (airbreakEnabled) { |
|
|
|
|
|
const view = getView(); |
|
|
lockedZ = view.getFloat32(positionAddr + 8, true); |
|
|
lockedHealth = view.getFloat32(healthAddr, true); |
|
|
console.log(`[AirBreak] ENABLED - Locked Z=${lockedZ.toFixed(2)}, HP=${lockedHealth.toFixed(2)}`); |
|
|
} |
|
|
|
|
|
document.getElementById('ce-airbreak-status').textContent = |
|
|
airbreakEnabled ? |
|
|
`FLYING! Z=${lockedZ.toFixed(1)} HP=${lockedHealth.toFixed(0)}` : |
|
|
`Ready! [RShift to fly]`; |
|
|
document.getElementById('ce-airbreak-status').style.color = airbreakEnabled ? '#ff0' : '#0f0'; |
|
|
console.log('[AirBreak]', airbreakEnabled ? 'ENABLED' : 'DISABLED'); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!airbreakEnabled) return; |
|
|
|
|
|
|
|
|
const code = e.code; |
|
|
if (code === 'KeyW') keysPressed.w = true; |
|
|
if (code === 'KeyS') keysPressed.s = true; |
|
|
if (code === 'KeyA') keysPressed.a = true; |
|
|
if (code === 'KeyD') keysPressed.d = true; |
|
|
if (code === 'Space') keysPressed.space = true; |
|
|
if (code === 'ShiftLeft') keysPressed.shift = true; |
|
|
}, true); |
|
|
|
|
|
window.addEventListener('keyup', (e) => { |
|
|
const code = e.code; |
|
|
if (code === 'KeyW') keysPressed.w = false; |
|
|
if (code === 'KeyS') keysPressed.s = false; |
|
|
if (code === 'KeyA') keysPressed.a = false; |
|
|
if (code === 'KeyD') keysPressed.d = false; |
|
|
if (code === 'Space') keysPressed.space = false; |
|
|
if (code === 'ShiftLeft') keysPressed.shift = false; |
|
|
}, true); |
|
|
|
|
|
|
|
|
setInterval(airbreakTick, 16); |
|
|
setInterval(updatePositionDisplay, 100); |
|
|
|
|
|
|
|
|
document.getElementById('ce-setup-airbreak').onclick = setupAirbreak; |
|
|
document.getElementById('ce-fly-speed').onchange = () => { |
|
|
flySpeed = parseFloat(document.getElementById('ce-fly-speed').value) || 2.0; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
if (isTouch) { |
|
|
const joystick = document.getElementById('airbreak-joystick'); |
|
|
const joystickKnob = document.getElementById('airbreak-joystick-knob'); |
|
|
const upBtn = document.getElementById('airbreak-up-btn'); |
|
|
const downBtn = document.getElementById('airbreak-down-btn'); |
|
|
const verticalBtns = document.getElementById('airbreak-vertical-btns'); |
|
|
|
|
|
let joystickActive = false; |
|
|
let joystickCenterX = 0; |
|
|
let joystickCenterY = 0; |
|
|
const joystickRadius = 60; |
|
|
const deadzone = 15; |
|
|
|
|
|
|
|
|
function updateTouchControlsVisibility() { |
|
|
if (airbreakConfigured && airbreakEnabled) { |
|
|
airbreakTouchControls.classList.add('active'); |
|
|
verticalBtns.style.display = 'flex'; |
|
|
} else { |
|
|
airbreakTouchControls.classList.remove('active'); |
|
|
verticalBtns.style.display = 'none'; |
|
|
} |
|
|
|
|
|
|
|
|
if (airbreakConfigured) { |
|
|
flyToggleBtn.classList.add('visible'); |
|
|
flyToggleBtn.classList.toggle('active', airbreakEnabled); |
|
|
flyToggleBtn.textContent = airbreakEnabled ? 'STOP' : 'FLY'; |
|
|
} else { |
|
|
flyToggleBtn.classList.remove('visible'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let flyBtnDragging = false; |
|
|
let flyBtnStartX = 0; |
|
|
let flyBtnStartY = 0; |
|
|
let flyBtnInitialRight = 0; |
|
|
let flyBtnInitialBottom = 0; |
|
|
|
|
|
|
|
|
const savedFlyPos = localStorage.getItem('cheat-fly-pos'); |
|
|
if (savedFlyPos) { |
|
|
try { |
|
|
const pos = JSON.parse(savedFlyPos); |
|
|
flyToggleBtn.style.right = pos.right + 'px'; |
|
|
flyToggleBtn.style.bottom = pos.bottom + 'px'; |
|
|
} catch(e) {} |
|
|
} |
|
|
|
|
|
flyToggleBtn.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
flyBtnDragging = false; |
|
|
|
|
|
const touch = e.touches[0]; |
|
|
flyBtnStartX = touch.clientX; |
|
|
flyBtnStartY = touch.clientY; |
|
|
|
|
|
|
|
|
const rect = flyToggleBtn.getBoundingClientRect(); |
|
|
flyBtnInitialRight = window.innerWidth - rect.right; |
|
|
flyBtnInitialBottom = window.innerHeight - rect.bottom; |
|
|
}, { passive: false }); |
|
|
|
|
|
flyToggleBtn.addEventListener('touchmove', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
|
|
|
const touch = e.touches[0]; |
|
|
const deltaX = touch.clientX - flyBtnStartX; |
|
|
const deltaY = touch.clientY - flyBtnStartY; |
|
|
|
|
|
|
|
|
if (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { |
|
|
flyBtnDragging = true; |
|
|
} |
|
|
|
|
|
if (flyBtnDragging) { |
|
|
let newRight = flyBtnInitialRight - deltaX; |
|
|
let newBottom = flyBtnInitialBottom - deltaY; |
|
|
|
|
|
|
|
|
const btnWidth = flyToggleBtn.offsetWidth; |
|
|
const btnHeight = flyToggleBtn.offsetHeight; |
|
|
newRight = Math.max(0, Math.min(window.innerWidth - btnWidth, newRight)); |
|
|
newBottom = Math.max(0, Math.min(window.innerHeight - btnHeight, newBottom)); |
|
|
|
|
|
flyToggleBtn.style.right = newRight + 'px'; |
|
|
flyToggleBtn.style.bottom = newBottom + 'px'; |
|
|
} |
|
|
}, { passive: false }); |
|
|
|
|
|
flyToggleBtn.addEventListener('touchend', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
|
|
|
if (flyBtnDragging) { |
|
|
|
|
|
const rect = flyToggleBtn.getBoundingClientRect(); |
|
|
localStorage.setItem('cheat-fly-pos', JSON.stringify({ |
|
|
right: window.innerWidth - rect.right, |
|
|
bottom: window.innerHeight - rect.bottom |
|
|
})); |
|
|
} else { |
|
|
|
|
|
if (airbreakConfigured) { |
|
|
airbreakEnabled = !airbreakEnabled; |
|
|
|
|
|
if (airbreakEnabled) { |
|
|
const view = getView(); |
|
|
lockedZ = view.getFloat32(positionAddr + 8, true); |
|
|
lockedHealth = view.getFloat32(healthAddr, true); |
|
|
console.log(`[AirBreak Touch] ENABLED - Locked Z=${lockedZ.toFixed(2)}, HP=${lockedHealth.toFixed(2)}`); |
|
|
} |
|
|
|
|
|
document.getElementById('ce-airbreak-status').textContent = |
|
|
airbreakEnabled ? |
|
|
`FLYING! Z=${lockedZ.toFixed(1)} HP=${lockedHealth.toFixed(0)}` : |
|
|
`Ready! [Tap FLY]`; |
|
|
document.getElementById('ce-airbreak-status').style.color = airbreakEnabled ? '#ff0' : '#0f0'; |
|
|
|
|
|
updateTouchControlsVisibility(); |
|
|
} |
|
|
} |
|
|
|
|
|
flyBtnDragging = false; |
|
|
}, { passive: false }); |
|
|
|
|
|
|
|
|
joystick.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
joystickActive = true; |
|
|
|
|
|
const rect = joystick.getBoundingClientRect(); |
|
|
joystickCenterX = rect.left + rect.width / 2; |
|
|
joystickCenterY = rect.top + rect.height / 2; |
|
|
|
|
|
handleJoystickMove(e.touches[0]); |
|
|
}, { passive: false }); |
|
|
|
|
|
joystick.addEventListener('touchmove', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
if (joystickActive) { |
|
|
handleJoystickMove(e.touches[0]); |
|
|
} |
|
|
}, { passive: false }); |
|
|
|
|
|
joystick.addEventListener('touchend', (e) => { |
|
|
e.preventDefault(); |
|
|
joystickActive = false; |
|
|
|
|
|
|
|
|
joystickKnob.style.transform = 'translate(-50%, -50%)'; |
|
|
|
|
|
|
|
|
keysPressed.w = false; |
|
|
keysPressed.s = false; |
|
|
keysPressed.a = false; |
|
|
keysPressed.d = false; |
|
|
}, { passive: false }); |
|
|
|
|
|
joystick.addEventListener('touchcancel', (e) => { |
|
|
joystickActive = false; |
|
|
joystickKnob.style.transform = 'translate(-50%, -50%)'; |
|
|
keysPressed.w = false; |
|
|
keysPressed.s = false; |
|
|
keysPressed.a = false; |
|
|
keysPressed.d = false; |
|
|
}); |
|
|
|
|
|
function handleJoystickMove(touch) { |
|
|
let deltaX = touch.clientX - joystickCenterX; |
|
|
let deltaY = touch.clientY - joystickCenterY; |
|
|
|
|
|
|
|
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); |
|
|
if (distance > joystickRadius) { |
|
|
deltaX = (deltaX / distance) * joystickRadius; |
|
|
deltaY = (deltaY / distance) * joystickRadius; |
|
|
} |
|
|
|
|
|
|
|
|
joystickKnob.style.transform = `translate(calc(-50% + ${deltaX}px), calc(-50% + ${deltaY}px))`; |
|
|
|
|
|
|
|
|
keysPressed.w = deltaY < -deadzone; |
|
|
keysPressed.s = deltaY > deadzone; |
|
|
keysPressed.a = deltaX < -deadzone; |
|
|
keysPressed.d = deltaX > deadzone; |
|
|
} |
|
|
|
|
|
|
|
|
upBtn.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
keysPressed.space = true; |
|
|
}, { passive: false }); |
|
|
|
|
|
upBtn.addEventListener('touchend', (e) => { |
|
|
e.preventDefault(); |
|
|
keysPressed.space = false; |
|
|
}, { passive: false }); |
|
|
|
|
|
upBtn.addEventListener('touchcancel', () => { |
|
|
keysPressed.space = false; |
|
|
}); |
|
|
|
|
|
downBtn.addEventListener('touchstart', (e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
keysPressed.shift = true; |
|
|
}, { passive: false }); |
|
|
|
|
|
downBtn.addEventListener('touchend', (e) => { |
|
|
e.preventDefault(); |
|
|
keysPressed.shift = false; |
|
|
}, { passive: false }); |
|
|
|
|
|
downBtn.addEventListener('touchcancel', () => { |
|
|
keysPressed.shift = false; |
|
|
}); |
|
|
|
|
|
|
|
|
const originalSetupAirbreak = setupAirbreak; |
|
|
setupAirbreak = function() { |
|
|
originalSetupAirbreak(); |
|
|
updateTouchControlsVisibility(); |
|
|
}; |
|
|
document.getElementById('ce-setup-airbreak').onclick = setupAirbreak; |
|
|
|
|
|
|
|
|
setInterval(updateTouchControlsVisibility, 500); |
|
|
} |
|
|
|
|
|
})(); |
|
|
|