Загрузка данных
require('samp.synchronization')
local sampfuncs = require('sampfuncs')
local raknet = require('samp.raknet')
local vector = require('vector3d')
local memory = require('memory')
local ffi = require('ffi')
-- stall diagnostics: which teleport call is followed by a game hang (frame gap > 0.7 s is printed with the last call)
local lastTpCall, lastTpCallAt = '', 0
do
local origChar, origCar = setCharCoordinates, setCarCoordinates
local function wrap(name, orig)
return function(...)
local line = debug.getinfo(2, 'l').currentline
local t0 = os.clock()
local r = orig(...)
local dt = os.clock() - t0
lastTpCall, lastTpCallAt = name .. ' line ' .. line, os.clock()
if dt > 0.3 then
sampAddChatMessage(string.format('{ff6969}[stall] %s line %d blocked %.1fs', name, line, dt), -1)
end
return r
end
end
setCharCoordinates = wrap('setCharCoordinates', origChar)
setCarCoordinates = wrap('setCarCoordinates', origCar)
end
local inicfg = require('inicfg')
local imgui = require('mimgui')
local encoding = require('encoding')
encoding.default = 'CP1251'
local u8 = encoding.UTF8
ffi.cdef[[
void keybd_event(uint8_t bVk, uint8_t bScan, uint32_t dwFlags, uintptr_t dwExtraInfo);
]]
function pressKeyDown(vk)
ffi.C.keybd_event(vk, 0, 0, 0)
end
function releaseKey(vk)
ffi.C.keybd_event(vk, 0, 2, 0)
end
local directIni = 'r-realtor by @purpurins.ini'
local ini = inicfg.load({
main = {
bar = true,
barX = 50,
barY = 500
},
catch = {
safeX = 0.0,
safeY = 0.0,
safeZ = 0.0,
curdStep = 0.7,
rtpStep = 9,
interiorX = -1297.3,
interiorY = -416.1,
interiorZ = 1001.0,
outsideX = -425.8,
outsideY = -1341.4,
outsideZ = 40.5,
checkRadius = 5.0,
exitTimeout = 10,
routeDelay = 50,
priceLimit = 200000,
autoPayday = false,
manualElevator = false,
apartmentIndex = 1,
carId = -1,
carOffset = 12.0,
curdPause = 1500
}
}, directIni)
inicfg.save(ini, directIni)
function SaveCfg()
inicfg.save(ini, directIni)
end
local teleport = {
bar_move = false,
boolean = false,
persent = 0,
packet = 0
}
local catch = {
active = false,
status = ''
}
local apartment = {
hunting = false,
items = {},
target = nil,
waitingElevator = false,
waitingBuy = false
}
local ECONOM_PICKUPS = {
{x = 2321.2, y = -2454.8, z = 1009.4},
{x = 2320.9, y = -2459.2, z = 1009.4},
{x = 2330.1, y = -2454.9, z = 1009.4},
{x = 2330.0, y = -2458.9, z = 1009.4}
}
local KVARTIRA_PICKUPS = {
{x = 3031.0, y = 1704.3, z = 997.5},
{x = 3028.7, y = 1707.1, z = 997.5},
{x = 3026.1, y = 1707.2, z = 997.5},
{x = 3023.8, y = 1704.2, z = 997.5}
}
-- CP1251 encoded strings for packet matching
local CP1251 = {
etazh = string.char(253, 242, 224, 230),
radare = string.char(240, 224, 228, 224, 240, 229),
svoboden = string.char(241, 226, 238, 225, 238, 228, 229, 237),
podyezd = string.char(207, 238, 228, 252, 229, 231, 228),
vzaimodeystvovat = string.char(194, 231, 224, 232, 236, 238, 228, 229, 233, 241, 242, 226, 238, 226, 224, 242, 252)
}
local showMenu = imgui.new.bool(false)
local showSettings = imgui.new.bool(false)
local buf = {
curdStep = imgui.new.float(ini.catch.curdStep),
rtpStep = imgui.new.int(ini.catch.rtpStep),
checkRadius = imgui.new.float(ini.catch.checkRadius),
exitTimeout = imgui.new.int(ini.catch.exitTimeout),
routeDelay = imgui.new.int(ini.catch.routeDelay),
priceLimit = imgui.new.int(ini.catch.priceLimit),
autoPayday = imgui.new.bool(ini.catch.autoPayday),
manualElevator = imgui.new.bool(ini.catch.manualElevator),
apartmentIndex = imgui.new.int(ini.catch.apartmentIndex),
interiorX = imgui.new.float(ini.catch.interiorX),
interiorY = imgui.new.float(ini.catch.interiorY),
interiorZ = imgui.new.float(ini.catch.interiorZ),
outsideX = imgui.new.float(ini.catch.outsideX),
outsideY = imgui.new.float(ini.catch.outsideY),
outsideZ = imgui.new.float(ini.catch.outsideZ),
safeX = imgui.new.float(ini.catch.safeX),
safeY = imgui.new.float(ini.catch.safeY),
safeZ = imgui.new.float(ini.catch.safeZ),
barShow = imgui.new.bool(ini.main.bar)
}
local ROUTE_1 = {
{x=-1295.7156,y=-416.8015,z=1001.0000},
{x=-1295.7213,y=-417.3696,z=1001.0000},
{x=-1295.7269,y=-417.9422,z=1001.0000},
{x=-1295.7336,y=-418.6414,z=1001.0000},
{x=-1295.7404,y=-419.3116,z=1001.0000},
{x=-1295.7472,y=-419.9932,z=1001.0000},
{x=-1295.7537,y=-420.6914,z=1001.0000},
{x=-1295.7604,y=-421.2037,z=1001.0000},
{x=-1296.0499,y=-421.8296,z=1001.0000},
{x=-1296.4739,y=-422.2974,z=1001.0000},
{x=-1297.0659,y=-422.6009,z=1001.0000},
{x=-1297.5638,y=-422.6944,z=1001.0000},
{x=-1298.2347,y=-422.3384,z=1001.0000},
{x=-1298.7526,y=-421.9099,z=1001.0000},
{x=-1299.2393,y=-421.4688,z=1001.0000},
{x=-1299.8873,y=-421.4061,z=1001.0000},
{x=-1300.5593,y=-421.4308,z=1001.0000},
{x=-1301.2802,y=-421.4572,z=1001.0000},
{x=-1301.9220,y=-421.4883,z=1001.0000},
{x=-1302.5580,y=-421.7512,z=1001.0000},
{x=-1302.7153,y=-422.3867,z=1001.0000},
{x=-1302.6377,y=-423.0829,z=1001.0000},
{x=-1302.8138,y=-423.7206,z=1001.0000},
{x=-1303.1964,y=-424.2230,z=1001.0000},
{x=-1303.5144,y=-424.6509,z=1001.0000},
{x=-1303.8748,y=-425.1795,z=1001.0000},
{x=-1304.1715,y=-425.5840,z=1001.0000},
{x=-1304.5883,y=-426.1485,z=1001.0000},
{x=-1304.9713,y=-426.6637,z=1001.0000},
{x=-1305.3905,y=-427.2000,z=1001.0000},
{x=-1305.8579,y=-427.7214,z=1001.0000},
{x=-1306.3307,y=-428.2000,z=1001.0000},
{x=-1306.7999,y=-428.6745,z=1001.0000},
{x=-1307.2588,y=-429.1119,z=1001.0000},
{x=-1307.7524,y=-429.5616,z=1001.0000},
{x=-1308.2727,y=-430.0351,z=1001.0000},
{x=-1308.8119,y=-430.5260,z=1001.0000},
{x=-1309.3169,y=-430.9858,z=1001.0000},
{x=-1309.8079,y=-431.4187,z=1001.0000},
{x=-1310.3145,y=-431.7969,z=1001.0000}
}
local ROUTE_2 = {
{x=-1295.8469,y=-417.0778,z=1005.0547},
{x=-1295.8411,y=-417.6600,z=1005.0547},
{x=-1295.9012,y=-418.2766,z=1005.0547},
{x=-1296.1851,y=-418.9017,z=1005.0547},
{x=-1296.6958,y=-419.3144,z=1005.0547},
{x=-1297.2224,y=-419.7321,z=1005.0547},
{x=-1297.7899,y=-420.1824,z=1005.0547},
{x=-1298.2262,y=-420.5285,z=1005.0547},
{x=-1298.6305,y=-420.8826,z=1005.0547},
{x=-1299.0696,y=-421.3048,z=1005.0547},
{x=-1299.5313,y=-421.7599,z=1005.0547},
{x=-1299.9503,y=-422.1730,z=1005.0547},
{x=-1300.4106,y=-422.6270,z=1005.0547},
{x=-1300.8479,y=-423.0581,z=1005.0547},
{x=-1301.2782,y=-423.4823,z=1005.0547},
{x=-1301.7518,y=-423.9495,z=1005.0547},
{x=-1302.1996,y=-424.3909,z=1005.0547},
{x=-1302.6555,y=-424.8404,z=1005.0547},
{x=-1303.1189,y=-425.2972,z=1005.0547},
{x=-1303.5433,y=-425.7156,z=1005.0547},
{x=-1303.9847,y=-426.1509,z=1005.0547},
{x=-1304.4431,y=-426.6029,z=1005.0547},
{x=-1304.8875,y=-427.0410,z=1005.0547},
{x=-1305.3370,y=-427.4844,z=1005.0547},
{x=-1305.7819,y=-427.9187,z=1005.0547},
{x=-1306.2023,y=-428.3069,z=1005.0547},
{x=-1306.7052,y=-428.7716,z=1005.0547},
{x=-1307.1713,y=-429.2023,z=1005.0547},
{x=-1307.6539,y=-429.6483,z=1005.0547},
{x=-1308.1357,y=-430.0823,z=1005.0547},
{x=-1308.6005,y=-430.4549,z=1005.0547},
{x=-1309.0978,y=-430.7673,z=1005.0547},
{x=-1309.5988,y=-431.0603,z=1005.0547},
{x=-1310.0538,y=-431.3443,z=1005.0547},
{x=-1310.4633,y=-431.6360,z=1005.0547},
{x=-1310.9218,y=-431.9683,z=1005.0547},
{x=-1310.9340,y=-431.9771,z=1003.0547},
{x=-1310.9340,y=-431.9771,z=1002.4662},
{x=-1310.9340,y=-431.9771,z=1001.7351},
{x=-1310.9395,y=-431.9811,z=1001.0000}
}
function selectRoute()
local px, py, pz = getCharCoordinates(PLAYER_PED)
if pz > 1003.0 then
return ROUTE_2, 2
end
return ROUTE_1, 1
end
function parseApartments(text)
for gid, atype, entrance, aptNum, price, flag, px, py in
text:gmatch('%[(%d+),(%d+),"(%d+)","[^#]+#%d+%. [^#]+#(%d+)",(%d+),(%d+),([%d%.%-]+),([%d%.%-]+)%]') do
table.insert(apartment.items, {
globalId = tonumber(gid),
aptType = tonumber(atype),
entrance = entrance,
aptNumber = tonumber(aptNum),
price = tonumber(price),
free = tonumber(flag) == 0,
posX = tonumber(px),
posY = tonumber(py)
})
end
end
function countFreeApartments()
local c = 0
for _, apt in ipairs(apartment.items) do
if apt.free and apt.price <= ini.catch.priceLimit then c = c + 1 end
end
return c
end
function findBestApartment()
local count = 0
for _, apt in ipairs(apartment.items) do
if apt.free and apt.price <= ini.catch.priceLimit then
count = count + 1
if count == ini.catch.apartmentIndex then
return apt
end
end
end
return nil
end
function isEconom(apt)
return apt.price <= 500000
end
function getPickupCoords(apt)
local idx = ((apt.aptNumber - 1) % 4) + 1
if isEconom(apt) then
return ECONOM_PICKUPS[idx]
else
return KVARTIRA_PICKUPS[idx]
end
end
function getFloor(apt)
return math.ceil(apt.aptNumber / 4)
end
local lastAltTime = 0
function sendAltKey()
-- one Alt per 1.5s: the Interactions hook and the elevator phase must not double-press
if os.clock() - lastAltTime < 1.5 then return end
lastAltTime = os.clock()
local bs = raknetNewBitStream()
local bytes = {215, 2, 0, 0, 0, 0, 0, 21, 0, 0, 0, 79, 110, 80, 108, 97, 121, 101, 114, 67, 108, 105, 101, 110, 116, 83, 105, 100, 101, 75, 101, 121, 2, 0, 0, 0, 100, 18, 0, 0, 0}
for _, b in ipairs(bytes) do raknetBitStreamWriteInt8(bs, b) end
raknetSendBitStream(bs)
raknetDeleteBitStream(bs)
end
function sendRealtorClose()
local bs = raknetNewBitStream()
local bytes = {215, 2, 0, 0, 0, 0, 0, 14, 0, 0, 0, 79, 110, 82, 101, 97, 108, 116, 111, 114, 67, 108, 111, 115, 101, 0, 0, 0, 0}
for _, b in ipairs(bytes) do raknetBitStreamWriteInt8(bs, b) end
raknetSendBitStream(bs)
raknetDeleteBitStream(bs)
end
function sendRealtorSelectCategory(cat)
local bs = raknetNewBitStream()
local bytes = {215, 2, 0, 0, 0, 0, 0, 23, 0, 0, 0, 79, 110, 82, 101, 97, 108, 116, 111, 114, 83, 101, 108, 101, 99, 116, 67, 97, 116, 101, 103, 111, 114, 121, 2, 0, 0, 0, 100}
for _, b in ipairs(bytes) do raknetBitStreamWriteInt8(bs, b) end
raknetBitStreamWriteInt8(bs, bit.band(cat, 0xFF))
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
raknetSendBitStream(bs)
raknetDeleteBitStream(bs)
end
function sendRealtorLoadMore(page)
local bs = raknetNewBitStream()
local bytes = {215, 2, 0, 0, 0, 0, 0, 17, 0, 0, 0, 79, 110, 82, 101, 97, 108, 116, 111, 114, 76, 111, 97, 100, 77, 111, 114, 101, 2, 0, 0, 0, 100}
for _, b in ipairs(bytes) do raknetBitStreamWriteInt8(bs, b) end
raknetBitStreamWriteInt8(bs, bit.band(page, 0xFF))
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
raknetSendBitStream(bs)
raknetDeleteBitStream(bs)
end
function autoLoadApartments()
lua_thread.create(function()
apartment.items = {}
sampAddChatMessage('{ffff69}[HUNT] Auto loading apartments...', -1)
sendRealtorSelectCategory(1)
wait(500)
for page = 1, 10 do
sendRealtorLoadMore(page)
wait(300)
end
wait(500)
local best = findBestApartment()
if best and apartment.hunting and not apartment.target then
apartment.target = best
sampAddChatMessage('{69ff69}[HUNT] Apt #' .. best.aptNumber .. ' Ent #' .. best.entrance .. ' Price: ' .. best.price, -1)
lua_thread.create(startApartmentCatch)
elseif not best then
sampAddChatMessage('{ff6969}[HUNT] No free apartments under ' .. ini.catch.priceLimit, -1)
end
end)
end
function sendDialogResponseFull(dialogId, button, listIndex, inputText)
local bs = raknetNewBitStream()
-- Header
local header = {215, 2, 0, 0, 0, 0, 0}
for _, b in ipairs(header) do raknetBitStreamWriteInt8(bs, b) end
-- "OnDialogResponse" string
local name = "OnDialogResponse"
raknetBitStreamWriteInt8(bs, #name)
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
for i = 1, #name do raknetBitStreamWriteInt8(bs, string.byte(name, i)) end
-- Params count
raknetBitStreamWriteInt8(bs, 8)
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
raknetBitStreamWriteInt8(bs, 0)
-- dialogId (dword)
raknetBitStreamWriteInt8(bs, 100)
raknetBitStreamWriteInt8(bs, bit.band(dialogId, 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(dialogId, 8), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(dialogId, 16), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(dialogId, 24), 0xFF))
-- button (dword)
raknetBitStreamWriteInt8(bs, 100)
raknetBitStreamWriteInt8(bs, bit.band(button, 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(button, 8), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(button, 16), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(button, 24), 0xFF))
-- listIndex (dword)
raknetBitStreamWriteInt8(bs, 100)
local li = listIndex
if li < 0 then li = 0xFFFFFFFF end
raknetBitStreamWriteInt8(bs, bit.band(li, 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(li, 8), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(li, 16), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(li, 24), 0xFF))
-- inputText (string with length prefix)
raknetBitStreamWriteInt8(bs, 115)
local str = inputText or ""
raknetBitStreamWriteInt8(bs, bit.band(#str, 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(#str, 8), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(#str, 16), 0xFF))
raknetBitStreamWriteInt8(bs, bit.band(bit.rshift(#str, 24), 0xFF))
for i = 1, #str do raknetBitStreamWriteInt8(bs, string.byte(str, i)) end
raknetSendBitStream(bs)
raknetDeleteBitStream(bs)
end
imgui.OnInitialize(function()
imgui.GetIO().IniFilename = nil
local style = imgui.GetStyle()
style.WindowRounding = 12
style.FrameRounding = 8
style.GrabRounding = 6
style.ScrollbarRounding = 8
style.ChildRounding = 8
style.PopupRounding = 8
style.ItemSpacing = imgui.ImVec2(10, 8)
style.WindowPadding = imgui.ImVec2(14, 14)
style.FramePadding = imgui.ImVec2(10, 6)
style.WindowBorderSize = 1
style.FrameBorderSize = 0
style.ScrollbarSize = 10
local colors = style.Colors
-- Background
colors[imgui.Col.WindowBg] = imgui.ImVec4(0.06, 0.06, 0.09, 0.96)
colors[imgui.Col.ChildBg] = imgui.ImVec4(0.08, 0.08, 0.12, 0.60)
colors[imgui.Col.PopupBg] = imgui.ImVec4(0.07, 0.07, 0.11, 0.96)
-- Borders
colors[imgui.Col.Border] = imgui.ImVec4(0.40, 0.20, 0.80, 0.30)
colors[imgui.Col.BorderShadow] = imgui.ImVec4(0.00, 0.00, 0.00, 0.00)
-- Text
colors[imgui.Col.Text] = imgui.ImVec4(0.93, 0.93, 0.97, 1.00)
colors[imgui.Col.TextDisabled] = imgui.ImVec4(0.45, 0.45, 0.55, 1.00)
-- Buttons
colors[imgui.Col.Button] = imgui.ImVec4(0.35, 0.15, 0.70, 0.65)
colors[imgui.Col.ButtonHovered] = imgui.ImVec4(0.45, 0.25, 0.85, 0.80)
colors[imgui.Col.ButtonActive] = imgui.ImVec4(0.55, 0.30, 0.95, 0.90)
-- Frames
colors[imgui.Col.FrameBg] = imgui.ImVec4(0.12, 0.10, 0.20, 0.80)
colors[imgui.Col.FrameBgHovered] = imgui.ImVec4(0.20, 0.15, 0.35, 0.80)
colors[imgui.Col.FrameBgActive] = imgui.ImVec4(0.25, 0.18, 0.45, 0.80)
-- Headers
colors[imgui.Col.Header] = imgui.ImVec4(0.30, 0.15, 0.60, 0.50)
colors[imgui.Col.HeaderHovered] = imgui.ImVec4(0.40, 0.20, 0.75, 0.60)
colors[imgui.Col.HeaderActive] = imgui.ImVec4(0.45, 0.25, 0.85, 0.70)
-- Separator
colors[imgui.Col.Separator] = imgui.ImVec4(0.40, 0.20, 0.80, 0.25)
colors[imgui.Col.SeparatorHovered] = imgui.ImVec4(0.50, 0.30, 0.90, 0.50)
-- Scrollbar
colors[imgui.Col.ScrollbarBg] = imgui.ImVec4(0.05, 0.05, 0.08, 0.50)
colors[imgui.Col.ScrollbarGrab] = imgui.ImVec4(0.35, 0.15, 0.70, 0.50)
colors[imgui.Col.ScrollbarGrabHovered] = imgui.ImVec4(0.45, 0.25, 0.85, 0.70)
colors[imgui.Col.ScrollbarGrabActive] = imgui.ImVec4(0.55, 0.30, 0.95, 0.90)
-- Slider
colors[imgui.Col.SliderGrab] = imgui.ImVec4(0.50, 0.25, 0.90, 0.80)
colors[imgui.Col.SliderGrabActive] = imgui.ImVec4(0.60, 0.35, 1.00, 1.00)
-- Checkmark
colors[imgui.Col.CheckMark] = imgui.ImVec4(0.60, 0.35, 1.00, 1.00)
-- Title
colors[imgui.Col.TitleBg] = imgui.ImVec4(0.10, 0.05, 0.20, 0.95)
colors[imgui.Col.TitleBgActive] = imgui.ImVec4(0.25, 0.12, 0.50, 0.95)
colors[imgui.Col.TitleBgCollapsed] = imgui.ImVec4(0.08, 0.04, 0.15, 0.70)
end)
local mainFrame = imgui.OnFrame(function() return showMenu[0] and not isPauseMenuActive() and not sampIsScoreboardOpen() end,
function(player)
player.HideCursor = imgui.IsMouseDown(1)
local resX, resY = getScreenResolution()
imgui.SetNextWindowPos(imgui.ImVec2(resX / 2, resY / 2), imgui.Cond.FirstUseEver, imgui.ImVec2(0.5, 0.5))
imgui.SetNextWindowSize(imgui.ImVec2(380, 0), imgui.Cond.FirstUseEver)
imgui.Begin(u8('r-realtor by @purpurins'), showMenu, imgui.WindowFlags.NoCollapse + imgui.WindowFlags.AlwaysAutoResize)
-- Status block
imgui.BeginChild(u8('##status'), imgui.ImVec2(-1, 45), true)
if apartment.hunting then
imgui.TextColored(imgui.ImVec4(0.40, 1.0, 0.40, 1.0), u8(' HUNTING'))
if apartment.target then
imgui.SameLine(150)
imgui.TextColored(imgui.ImVec4(1.0, 0.85, 0.30, 1.0), u8('Apt #' .. apartment.target.aptNumber .. ' | Ent #' .. apartment.target.entrance .. ' | ' .. apartment.target.price))
end
elseif catch.active then
imgui.TextColored(imgui.ImVec4(1.0, 1.0, 0.40, 1.0), u8(' ' .. catch.status))
elseif teleport.boolean then
imgui.TextColored(imgui.ImVec4(1.0, 1.0, 0.40, 1.0), u8(' Teleporting ' .. teleport.persent .. '%'))
else
local vehId = false
if isCharOnFoot(PLAYER_PED) then vehId = getVehicle(200, true, false) end
if vehId then
imgui.TextColored(imgui.ImVec4(0.40, 1.0, 0.40, 1.0), u8(' Ready'))
imgui.SameLine(150)
imgui.TextColored(imgui.ImVec4(0.70, 0.70, 0.80, 1.0), u8('Vehicle: ' .. vehId))
else
imgui.TextColored(imgui.ImVec4(1.0, 0.40, 0.40, 1.0), u8(' No vehicle nearby'))
end
end
imgui.EndChild()
imgui.Spacing()
-- Apartment Catcher
imgui.TextColored(imgui.ImVec4(0.65, 0.45, 1.0, 1.0), u8('APARTMENT CATCHER'))
imgui.Separator()
imgui.Spacing()
imgui.TextColored(imgui.ImVec4(0.70, 0.70, 0.80, 1.0), u8('Limit: ' .. ini.catch.priceLimit .. ' | Free: ' .. countFreeApartments() .. ' | Order: #' .. ini.catch.apartmentIndex))
imgui.Spacing()
if apartment.hunting then
imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.70, 0.15, 0.15, 0.80))
imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.85, 0.20, 0.20, 0.90))
imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.95, 0.25, 0.25, 1.00))
if imgui.Button(u8(' Stop Hunting'), imgui.ImVec2(-1, 35)) then
apartment.hunting = false
apartment.target = nil
apartment.waitingElevator = false
apartment.waitingBuy = false
catch.active = false
teleport.boolean = false
catch.status = ''
end
imgui.PopStyleColor(3)
else
imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.15, 0.55, 0.15, 0.80))
imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.20, 0.70, 0.20, 0.90))
imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.25, 0.80, 0.25, 1.00))
if imgui.Button(u8(' Start Hunting'), imgui.ImVec2(-1, 35)) then
startHunting()
end
imgui.PopStyleColor(3)
if #apartment.items > 0 and not catch.active and not teleport.boolean then
if imgui.Button(u8(' Test (cached)'), imgui.ImVec2(-1, 28)) then
startTestHunt()
end
end
end
imgui.Spacing()
imgui.Spacing()
-- Teleport
imgui.TextColored(imgui.ImVec4(0.65, 0.45, 1.0, 1.0), u8('TELEPORT'))
imgui.Separator()
imgui.Spacing()
if catch.active or teleport.boolean then
imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.70, 0.15, 0.15, 0.80))
imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.85, 0.20, 0.20, 0.90))
imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.95, 0.25, 0.25, 1.00))
if imgui.Button(u8(' Stop'), imgui.ImVec2(-1, 30)) then
catch.active = false
teleport.boolean = false
catch.status = ''
end
imgui.PopStyleColor(3)
else
if imgui.Button(u8(' Catch (route)'), imgui.ImVec2(-1, 28)) then startCatch() end
if imgui.Button(u8(' TP to mark'), imgui.ImVec2(-1, 28)) then startMarkTp() end
imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.15, 0.40, 0.65, 0.80))
imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.20, 0.50, 0.80, 0.90))
imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.25, 0.55, 0.90, 1.00))
if imgui.Button(u8(' Bus'), imgui.ImVec2(170, 28)) then
startTpToCoords(-400.3, -1333.9, 41.1)
end
imgui.SameLine()
if imgui.Button(u8(' Arz'), imgui.ImVec2(170, 28)) then
startTpToCoords(648.566, 652.348, 12.0)
end
imgui.PopStyleColor(3)
end
imgui.Spacing()
imgui.Spacing()
-- Info
imgui.TextColored(imgui.ImVec4(0.65, 0.45, 1.0, 1.0), u8('INFO'))
imgui.Separator()
imgui.Spacing()
imgui.BeginChild(u8('##info'), imgui.ImVec2(-1, 75), true)
local px, py, pz = getCharCoordinates(PLAYER_PED)
imgui.TextColored(imgui.ImVec4(0.60, 0.60, 0.70, 1.0), u8(string.format('Pos: %.1f, %.1f, %.1f', px, py, pz)))
imgui.TextColored(imgui.ImVec4(0.60, 0.60, 0.70, 1.0), u8(string.format('Safe: %.1f, %.1f, %.1f', ini.catch.safeX, ini.catch.safeY, ini.catch.safeZ)))
local _, rn = selectRoute()
imgui.TextColored(imgui.ImVec4(0.60, 0.60, 0.70, 1.0), u8('Route: #' .. rn))
imgui.EndChild()
imgui.Spacing()
if imgui.Button(u8(' Save safe pos'), imgui.ImVec2(-1, 25)) then
local sx, sy, sz = getCharCoordinates(PLAYER_PED)
ini.catch.safeX, ini.catch.safeY, ini.catch.safeZ = sx, sy, sz
buf.safeX[0], buf.safeY[0], buf.safeZ[0] = sx, sy, sz
SaveCfg()
sampAddChatMessage(string.format('{69ff69}[r-realtor by @purpurins] Safe: %.1f, %.1f, %.1f', sx, sy, sz), -1)
end
imgui.Spacing()
imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.20, 0.15, 0.35, 0.80))
imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.30, 0.20, 0.50, 0.90))
imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.35, 0.25, 0.60, 1.00))
if imgui.Button(u8(' Settings'), imgui.ImVec2(-1, 25)) then showSettings[0] = not showSettings[0] end
imgui.PopStyleColor(3)
imgui.End()
end)
local settingsFrame = imgui.OnFrame(function() return showSettings[0] and showMenu[0] and not isPauseMenuActive() end,
function(player)
player.HideCursor = imgui.IsMouseDown(1)
local resX, resY = getScreenResolution()
imgui.SetNextWindowPos(imgui.ImVec2(resX / 2 + 190, resY / 2 - 100), imgui.Cond.FirstUseEver)
imgui.SetNextWindowSize(imgui.ImVec2(320, 0), imgui.Cond.FirstUseEver)
imgui.Begin(u8('Settings'), showSettings, imgui.WindowFlags.NoCollapse + imgui.WindowFlags.AlwaysAutoResize)
if imgui.CollapsingHeader(u8('Apartment')) then
if imgui.InputInt(u8('Price limit'), buf.priceLimit, 50000, 100000) then
if buf.priceLimit[0] < 0 then buf.priceLimit[0] = 0 end
ini.catch.priceLimit = buf.priceLimit[0]
SaveCfg()
end
if imgui.Checkbox(u8('Auto payday tabs'), buf.autoPayday) then
ini.catch.autoPayday = buf.autoPayday[0]
SaveCfg()
end
if imgui.Checkbox(u8('Walk to elevator myself'), buf.manualElevator) then
ini.catch.manualElevator = buf.manualElevator[0]
SaveCfg()
end
if imgui.InputInt(u8('Apartment # (order)'), buf.apartmentIndex, 1, 1) then
if buf.apartmentIndex[0] < 1 then buf.apartmentIndex[0] = 1 end
ini.catch.apartmentIndex = buf.apartmentIndex[0]
SaveCfg()
end
end
if imgui.CollapsingHeader(u8('General')) then
if imgui.Checkbox(u8('Show bar'), buf.barShow) then ini.main.bar = buf.barShow[0]; SaveCfg() end
if imgui.SliderFloat(u8('Curd step'), buf.curdStep, 0.1, 2.0, '%.1f') then ini.catch.curdStep = buf.curdStep[0]; SaveCfg() end
if imgui.SliderInt(u8('RTP step'), buf.rtpStep, 1, 20) then ini.catch.rtpStep = buf.rtpStep[0]; SaveCfg() end
if imgui.SliderFloat(u8('Check radius'), buf.checkRadius, 1.0, 20.0, '%.1f') then ini.catch.checkRadius = buf.checkRadius[0]; SaveCfg() end
if imgui.SliderInt(u8('Exit timeout'), buf.exitTimeout, 1, 30) then ini.catch.exitTimeout = buf.exitTimeout[0]; SaveCfg() end
if imgui.SliderInt(u8('Route delay'), buf.routeDelay, 10, 200) then ini.catch.routeDelay = buf.routeDelay[0]; SaveCfg() end
end
if imgui.CollapsingHeader(u8('Coords')) then
if imgui.InputFloat(u8('Int X'), buf.interiorX, 0.1, 1.0, '%.1f') then ini.catch.interiorX = buf.interiorX[0]; SaveCfg() end
if imgui.InputFloat(u8('Int Y'), buf.interiorY, 0.1, 1.0, '%.1f') then ini.catch.interiorY = buf.interiorY[0]; SaveCfg() end
if imgui.InputFloat(u8('Int Z'), buf.interiorZ, 0.1, 1.0, '%.1f') then ini.catch.interiorZ = buf.interiorZ[0]; SaveCfg() end
if imgui.InputFloat(u8('Out X'), buf.outsideX, 0.1, 1.0, '%.1f') then ini.catch.outsideX = buf.outsideX[0]; SaveCfg() end
if imgui.InputFloat(u8('Out Y'), buf.outsideY, 0.1, 1.0, '%.1f') then ini.catch.outsideY = buf.outsideY[0]; SaveCfg() end
if imgui.InputFloat(u8('Out Z'), buf.outsideZ, 0.1, 1.0, '%.1f') then ini.catch.outsideZ = buf.outsideZ[0]; SaveCfg() end
if imgui.InputFloat(u8('Safe X'), buf.safeX, 0.1, 1.0, '%.1f') then ini.catch.safeX = buf.safeX[0]; SaveCfg() end
if imgui.InputFloat(u8('Safe Y'), buf.safeY, 0.1, 1.0, '%.1f') then ini.catch.safeY = buf.safeY[0]; SaveCfg() end
if imgui.InputFloat(u8('Safe Z'), buf.safeZ, 0.1, 1.0, '%.1f') then ini.catch.safeZ = buf.safeZ[0]; SaveCfg() end
end
imgui.End()
end)
local freezeDbg = false
local freezeSeen, freezeSeenAt = {}, 0
local tpSince = nil
function main()
repeat wait(0) until isSampAvailable()
lua_thread.create(infobar)
sampRegisterChatCommand('rtp', function(arg)
if arg == '' then blip, x, y, z = getMark()
elseif arg == 'chp' then blip, x, y, z = getCheckPoint()
elseif arg == 'bar' then ini.main.bar = not ini.main.bar; buf.barShow[0] = ini.main.bar; SaveCfg(); return
elseif arg == 'bar move' then teleport.bar_move = not teleport.bar_move; return
else return end
if blip and not teleport.boolean then
lua_thread.create(function() carTp(x, y, z) end)
end
end)
sampRegisterChatCommand('rsave', function()
local sx, sy, sz = getCharCoordinates(PLAYER_PED)
ini.catch.safeX, ini.catch.safeY, ini.catch.safeZ = sx, sy, sz
buf.safeX[0], buf.safeY[0], buf.safeZ[0] = sx, sy, sz
SaveCfg()
sampAddChatMessage(string.format('{69ff69}[r-realtor by @purpurins] Safe: %.1f, %.1f, %.1f', sx, sy, sz), -1)
end)
sampRegisterChatCommand('rcar', function()
if carDriving() then
local ok, id = sampGetVehicleIdByCarHandle(storeCarCharIsInNoSave(PLAYER_PED))
if ok then
ini.catch.carId = id
SaveCfg()
sampAddChatMessage('{69ff69}[r-realtor by @purpurins] Car saved: id ' .. id, -1)
return
end
end
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] Sit in the car (driver) and use /rcar', -1)
end)
sampRegisterChatCommand('rfake', function()
fakeTpEnabled = not fakeTpEnabled
sampAddChatMessage('{ffff69}[r-realtor] fake tp (local ped stays, server moves): ' .. (fakeTpEnabled and 'on' or 'off'), -1)
end)
sampRegisterChatCommand('rfdbg', function()
freezeDbg = not freezeDbg
sampAddChatMessage('{ffff69}[freeze dbg] ' .. (freezeDbg and 'on' or 'off'), -1)
end)
sampRegisterChatCommand('rr', function()
showMenu[0] = not showMenu[0]
end)
sampAddChatMessage('{69ff69}[r-realtor by @purpurins] {FFFFFF}/rr - menu', -1)
wait(-1)
end
-- the server freezes the player for ~10 s after a teleport (TogglePlayerControllable, rpc 15): ignore it around our teleports
local freezeGuardUntil = 0
-- fake position mode: the server sees the player at fake.x/y/z (onfoot sync sent by us), the local ped does NOT move,
-- so the game never streams the far area (that is what hangs the client after a long teleport)
fake = {on = false, x = 0, y = 0, z = 0}
fakeTpEnabled = true
function sendVehicle(x, y, z)
local p = createSync('vehicle')
p.position = {x, y, z}
p.moveSpeed = {0.0, 0.0, 0.0}
p.send()
end
function fakeStart(x, y, z, car)
fake.x, fake.y, fake.z = x, y, z
fake.car = car and true or false
if fake.on then return end
fake.on = true
lua_thread.create(function()
while fake.on do
if fake.car and isCharInAnyCar(PLAYER_PED) then
sendVehicle(fake.x, fake.y, fake.z)
else
sendPlayer(fake.x, fake.y, fake.z, false)
end
wait(50)
end
end)
end
-- stop the fake mode; real = true: move the local ped to the fake point (one real teleport, as before)
function fakeStop(real)
if not fake.on then return end
fake.on = false
if real then
if fake.car and isCharInAnyCar(PLAYER_PED) then
setCarCoordinates(storeCarCharIsInNoSave(PLAYER_PED), fake.x, fake.y, fake.z)
else
setCharCoordinates(PLAYER_PED, fake.x, fake.y, fake.z)
end
end
end
-- keys the script presses by keybd_event (Alt, arrows, Enter): make sure none is left held down
function releaseStuckKeys()
for _, vk in ipairs({0xA4, 0xA5, 0x28, 0x0D}) do
releaseKey(vk)
end
end
local unfreezeRunning = false
function guardFreeze(sec)
freezeGuardUntil = os.clock() + (sec or 20)
if unfreezeRunning then return end
unfreezeRunning = true
-- keep the player free while the guard is active: control on, position not frozen (no scene loading here)
lua_thread.create(function()
releaseStuckKeys()
while os.clock() < freezeGuardUntil do
setPlayerControl(PLAYER_HANDLE, true)
freezeCharPosition(PLAYER_PED, false)
wait(100)
end
releaseStuckKeys()
unfreezeRunning = false
end)
end
function onReceiveRpc(id, bs)
-- any server-side move (leaving the realtor office, interior doors): same guard window
if id == 12 then
fake.on = false
guardFreeze(15)
end
if freezeDbg and os.clock() < freezeGuardUntil and id ~= 12 then
-- every rpc id once per guard window
if freezeSeenAt ~= freezeGuardUntil then
freezeSeen = {}
freezeSeenAt = freezeGuardUntil
end
if not freezeSeen[id] then
freezeSeen[id] = true
sampAddChatMessage('{ffff69}[freeze dbg] rpc ' .. id, -1)
end
end
if id == 15 and (apartment.hunting or teleport.boolean or os.clock() < freezeGuardUntil) then
return false
end
end
function onServerMessage(color, text)
if ini.catch.autoPayday and apartment.hunting and not apartment.target then
local bankCheck = string.char(193, 192, 205, 202, 206, 194, 209, 202, 200, 201) -- БАНКОВСКИЙ in CP1251
if text:find(bankCheck) then
sampAddChatMessage('{ffff69}[HUNT] Payday detected! Auto loading...', -1)
autoLoadApartments()
end
end
end
function onReceivePacket(id, bs)
if id == 41 then -- connection accepted (reconnect): drop a teleport flag left from before
fake.on = false
teleport.boolean = false
teleport.packet = 0
end
local len = raknetBitStreamGetNumberOfBytesUsed(bs)
if len < 10 then return end
raknetBitStreamResetReadPointer(bs)
local text = ''
for i = 1, len do
local b = raknetBitStreamReadInt8(bs)
if b < 0 then b = b + 256 end
if b >= 32 then text = text .. string.char(b) end
end
raknetBitStreamResetReadPointer(bs)
if freezeDbg and id == 215 and os.clock() < freezeGuardUntil then
sampAddChatMessage('{ffff69}[freeze dbg] cef ' .. text:sub(1, 90), -1)
end
-- Parse apartment listings from CEF
if text:find('setPropertyItems') then
apartment.items = {}
parseApartments(text)
elseif text:find('addPropertyItems') then
parseApartments(text)
end
-- Auto-select apartment when hunting
if apartment.hunting and not apartment.target then
if text:find('PropertyItems') then
local best = findBestApartment()
if best then
apartment.target = best
sampAddChatMessage('{69ff69}[HUNT] Apt #' .. best.aptNumber .. ' Ent #' .. best.entrance .. ' Price: ' .. best.price, -1)
lua_thread.create(startApartmentCatch)
end
end
end
-- Auto elevator - simulate arrow keys
if apartment.waitingElevator and text:find('addDialogInQueue') and (text:find(CP1251.etazh) or text:find('FFCD00')) then
local target = apartment.target
if target then
local floor = getFloor(target)
local floorIndex = floor - 1
lua_thread.create(function()
wait(100)
-- Press Down arrow to select floor
for i = 1, floorIndex do
pressKeyDown(0x28) -- VK_DOWN
wait(10)
releaseKey(0x28)
wait(15)
end
wait(30)
-- Press Enter to confirm
pressKeyDown(0x0D) -- VK_RETURN
wait(10)
releaseKey(0x0D)
sampAddChatMessage('{69ff69}[HUNT] Floor ' .. floor, -1)
apartment.waitingElevator = false
wait(2000)
if apartment.hunting and target then
local pickup = getPickupCoords(target)
if pickup then
setCharCoordinates(PLAYER_PED, pickup.x, pickup.y, pickup.z)
apartment.waitingBuy = true
sampAddChatMessage('{69ff69}[HUNT] At pickup, waiting...', -1)
end
end
end)
end
end
-- Interactions [[7,"Взаимодействовать"]] -> answer with the Alt key packet
if apartment.hunting and text:find('Interactions', 1, true) and text:find(CP1251.vzaimodeystvovat, 1, true) then
lua_thread.create(function()
wait(100)
sendAltKey()
sampAddChatMessage('{69ff69}[HUNT] Interaction -> Alt sent', -1)
end)
end
-- Auto close "marked on radar" dialog
if apartment.hunting and text:find('addDialogInQueue') and text:find(CP1251.radare) then
lua_thread.create(function()
wait(300)
sendDialogResponseFull(0, 1, -1, "")
end)
return false
end
-- Auto Alt when at apartment pickup
if apartment.waitingBuy and text:find('Appartam') then
lua_thread.create(function()
wait(100)
pressKeyDown(0xA4) -- VK_LMENU (Left Alt)
wait(30)
releaseKey(0xA4)
sampAddChatMessage('{69ff69}[HUNT] At apartment, pressing Alt...', -1)
end)
end
-- Auto buy
if apartment.waitingBuy and text:find('addDialogInQueue') and (text:find(CP1251.svoboden) or text:find('33CC00')) then
lua_thread.create(function()
wait(500)
sendDialogResponseFull(0, 1, -1, "")
sampAddChatMessage('{69ff69}[HUNT] BOUGHT!', -1)
apartment.waitingBuy = false
apartment.hunting = false
apartment.target = nil
end)
return false
end
end
function startHunting()
if apartment.hunting then return end
apartment.hunting = true
apartment.target = nil
apartment.items = {}
apartment.waitingElevator = false
apartment.waitingBuy = false
sampAddChatMessage('{ffff69}[HUNT] Started! Open CEF > Квартиры > Свободное имущество', -1)
sampAddChatMessage('{ffff69}[HUNT] Price limit: ' .. ini.catch.priceLimit, -1)
end
function startTestHunt()
if #apartment.items == 0 then
sampAddChatMessage('{ff6969}[TEST] No cached data! Open CEF first.', -1)
return
end
local best = findBestApartment()
if not best then
sampAddChatMessage('{ff6969}[TEST] No free apartments in cache under ' .. ini.catch.priceLimit, -1)
return
end
apartment.hunting = true
apartment.target = best
apartment.waitingElevator = false
apartment.waitingBuy = false
sampAddChatMessage('{ffff69}[TEST] Apt #' .. best.aptNumber .. ' Ent #' .. best.entrance .. ' Price: ' .. best.price, -1)
lua_thread.create(startApartmentCatch)
end
function startApartmentCatch()
local target = apartment.target
if not target then return end
-- apartment found: close the realtor tablet right away
sendRealtorClose()
-- Phase 1: Route out of realtor
local routePoints, routeNum = selectRoute()
sampAddChatMessage('{ffff69}[HUNT] Using route ' .. routeNum, -1)
if #routePoints > 0 then
catch.active = true
catch.status = 'Route out...'
for i, point in ipairs(routePoints) do
if not apartment.hunting then break end
catch.status = 'Route ' .. i .. '/' .. #routePoints
setCharCoordinates(PLAYER_PED, point.x, point.y, point.z)
wait(ini.catch.routeDelay)
end
catch.active = false
if not apartment.hunting then return end
catch.status = 'Waiting exit...'
local timeout = os.clock() + ini.catch.exitTimeout
while os.clock() < timeout and apartment.hunting do
wait(100)
local px, py, pz = getCharCoordinates(PLAYER_PED)
if getDistanceBetweenCoords3d(px, py, pz, ini.catch.outsideX, ini.catch.outsideY, ini.catch.outsideZ) <= ini.catch.checkRadius then break end
end
local px, py, pz = getCharCoordinates(PLAYER_PED)
if getDistanceBetweenCoords3d(px, py, pz, ini.catch.outsideX, ini.catch.outsideY, ini.catch.outsideZ) > ini.catch.checkRadius then
sampAddChatMessage('{ff6969}[HUNT] Exit failed!', -1)
apartment.hunting = false
return
end
end
if not apartment.hunting then return end
-- Phase 2: RTP to entrance
sampAddChatMessage('{ffff69}[HUNT] RTP to entrance...', -1)
wait(300)
-- the saved car (/rcar): get in, drive-tp near the entrance, get out, curd on foot to the entrance coords
if not enterSavedCar() then apartment.hunting = false return end
local carHandle = storeCarCharIsInNoSave(PLAYER_PED)
-- the car stops carOffset metres short of the entrance (on the line from the start), the rest is on foot
local sx, sy, sz = getCarCoordinates(carHandle)
local dirx, diry = sx - target.posX, sy - target.posY
local dl = math.sqrt(dirx * dirx + diry * diry)
if dl < 1 then dl = 1 end
local off = math.min(ini.catch.carOffset, dl)
local carX = target.posX + dirx / dl * off
local carY = target.posY + diry / dl * off
local entranceZ = getGroundZFor3dCoord(target.posX + 2, target.posY + 2, 100.0)
if entranceZ == 0 then entranceZ = 15.0 end
entranceZ = entranceZ + 1.0
if not carTp(carX, carY, entranceZ, false) then apartment.hunting = false return end
if not apartment.hunting then return end
-- get out of the car
taskLeaveAnyCar(PLAYER_PED)
local t0 = os.clock()
while isCharInAnyCar(PLAYER_PED) and os.clock() - t0 < 3 do wait(50) end
if isCharInAnyCar(PLAYER_PED) then
local cx, cy, cz = getCarCoordinates(carHandle)
warpCharFromCarToCoord(PLAYER_PED, cx + 2, cy, cz)
end
wait(100)
-- the scene is loaded now: real ground at the entrance, then curd on foot to the exact coords
local gz = getGroundZFor3dCoord(target.posX, target.posY, entranceZ + 5)
if gz ~= 0 then entranceZ = gz + 1.0 end
guardFreeze()
-- always twice, just in case (no distance check)
for _ = 1, 2 do
if not apartment.hunting then return end
curdToPoint(target.posX, target.posY, entranceZ)
wait(ini.catch.curdPause or 1500)
end
sampAddChatMessage('{ffff69}[HUNT] At the entrance', -1)
-- Phase 3: Wait 4s after arriving at the house, then teleport to elevator
sampAddChatMessage('{ffff69}[HUNT] Arrived, elevator in 4s...', -1)
wait(4000)
if not apartment.hunting then return end
-- Elevator: either walk there yourself (manualElevator) or auto-move
local elevCoords
if isEconom(target) then
elevCoords = {x = 2325.9, y = -2454.3, z = 997.6}
else
elevCoords = {x = 2769.0, y = 1700.9, z = 997.5}
end
local elevTimeout = 15
if ini.catch.manualElevator then
fakeStop(true) -- walking yourself: the local ped has to be at the entrance
sampAddChatMessage('{ffff69}[HUNT] Walk to the elevator and press Alt, floor is picked automatically', -1)
apartment.waitingElevator = true
elevTimeout = 60
else
sampAddChatMessage('{ffff69}[HUNT] Moving to elevator...', -1)
guardFreeze()
fakeStop(false) -- from here the real position is sent again
safeSetPos(elevCoords.x, elevCoords.y, elevCoords.z)
wait(200)
setCharCoordinates(PLAYER_PED, elevCoords.x, elevCoords.y, elevCoords.z)
wait(300)
-- The Interactions hook normally sends Alt itself; send ours only if it did not (two Alts close the dialog)
local tpAt = os.clock()
apartment.waitingElevator = true
while os.clock() - tpAt < 1.5 and lastAltTime < tpAt and apartment.hunting do wait(50) end
if lastAltTime < tpAt then sendAltKey() end
end
-- Wait for elevator dialog
sampAddChatMessage('{ffff69}[HUNT] Waiting elevator...', -1)
local timeout = os.clock() + elevTimeout
while os.clock() < timeout and apartment.waitingElevator and apartment.hunting do
wait(200)
end
if apartment.waitingElevator then
sampAddChatMessage('{ff6969}[HUNT] Elevator timeout!', -1)
apartment.hunting = false
apartment.waitingElevator = false
end
end
function startCatch()
if catch.active or teleport.boolean then return end
if ini.catch.safeX == 0.0 and ini.catch.safeY == 0.0 and ini.catch.safeZ == 0.0 then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] No safe pos!', -1)
return
end
local routePoints, routeNum = selectRoute()
if #routePoints == 0 then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] No route file!', -1)
return
end
catch.active = true
lua_thread.create(function()
catch.status = 'Following route...'
for i, point in ipairs(routePoints) do
if not catch.active then break end
catch.status = 'Route ' .. i .. '/' .. #routePoints
setCharCoordinates(PLAYER_PED, point.x, point.y, point.z)
wait(ini.catch.routeDelay)
end
if not catch.active then return end
catch.status = 'Waiting exit...'
local timeout = os.clock() + ini.catch.exitTimeout
while os.clock() < timeout and catch.active do
wait(100)
local px, py, pz = getCharCoordinates(PLAYER_PED)
if getDistanceBetweenCoords3d(px, py, pz, ini.catch.outsideX, ini.catch.outsideY, ini.catch.outsideZ) <= ini.catch.checkRadius then break end
end
local px, py, pz = getCharCoordinates(PLAYER_PED)
if getDistanceBetweenCoords3d(px, py, pz, ini.catch.outsideX, ini.catch.outsideY, ini.catch.outsideZ) > ini.catch.checkRadius then
catch.status = 'Exit failed!'
catch.active = false
return
end
catch.status = 'RTP to safe...'
wait(300)
catch.status = 'Saved car...'
if not enterSavedCar() then catch.active = false; catch.status = ''; return end
catch.active = false
catch.status = ''
carTp(ini.catch.safeX, ini.catch.safeY, ini.catch.safeZ)
end)
end
-- slap the player under the map so the server sees it before the teleport (packets are let through while slapping)
function slapUnderMap()
guardFreeze()
local was = teleport.boolean
teleport.boolean = false
local x, y, z = getCharCoordinates(PLAYER_PED)
setCharCoordinates(PLAYER_PED, x, y, z - 3.0)
wait(300)
teleport.boolean = was
end
function startMarkTp()
local blip, mx, my, mz = getMark()
if not blip then sampAddChatMessage('{ff6969}[r-realtor by @purpurins] No mark!', -1); return end
if teleport.boolean then return end
lua_thread.create(function() carTp(mx, my, mz) end)
end
function startTpToCoords(tx, ty, tz)
if teleport.boolean then return end
lua_thread.create(function() carTp(tx, ty, tz) end)
end
-- car-only teleport: the player drives, vehicle sync in RTP_STEP-metre steps, one step per frame (no delays)
-- sit (driver) in the car saved with /rcar; call from a lua thread. returns true when sitting in it
function enterSavedCar()
local savedId = tonumber(ini.catch.carId) or -1
if savedId < 0 then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] No saved car (sit in it and use /rcar)', -1)
return false
end
-- the interior / dimension changes with a delay after leaving: wait for the outside world, then for the car to stream in
local t0 = os.clock()
while getActiveInterior() ~= 0 and os.clock() - t0 < 10 do wait(100) end
local okCar, carHandle = false, nil
t0 = os.clock()
while os.clock() - t0 < 10 do
okCar, carHandle = sampGetCarHandleBySampVehicleId(savedId)
if okCar and doesVehicleExist(carHandle) then break end
okCar = false
wait(200)
end
if not okCar then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] Saved car not found (save it with /rcar while sitting in it, stand near it)', -1)
return false
end
if not (isCharInAnyCar(PLAYER_PED) and storeCarCharIsInNoSave(PLAYER_PED) == carHandle) then
local drv = getDriverOfCar(carHandle)
if drv ~= -1 and drv ~= PLAYER_PED then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] Saved car is occupied', -1)
return false
end
sampSendEnterVehicle(savedId, false)
wait(150)
warpCharIntoCar(PLAYER_PED, carHandle)
wait(300)
end
if not carDriving() then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] Could not sit in the saved car', -1)
return false
end
return true
end
function carDriving()
return isCharInAnyCar(PLAYER_PED) and getDriverOfCar(storeCarCharIsInNoSave(PLAYER_PED)) == PLAYER_PED
end
function carTp(tx, ty, tz, fakeEnd)
if teleport.boolean then return false end
if not carDriving() then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] Teleport works only while driving a car!', -1)
return false
end
local car = storeCarCharIsInNoSave(PLAYER_PED)
local mx, my, mz = getCarCoordinates(car)
local step = ini.catch.rtpStep
local startDist = getDistanceBetweenCoords3d(mx, my, mz, tx, ty, tz)
local time = os.time()
teleport.boolean = true
teleport.persent = 0
guardFreeze()
local streamed = false
local sent, BURST = 0, 8
while getDistanceBetweenCoords3d(mx, my, mz, tx, ty, tz) >= step and teleport.boolean do
-- stream the destination only when little is left
if not streamed and not fakeEnd and getDistanceBetweenCoords3d(mx, my, mz, tx, ty, tz) < step * 4 then
streamed = true
requestCollision(tx, ty)
loadScene(tx, ty, tz)
end
teleport.persent = math.floor(100 - (getDistanceBetweenCoords3d(mx, my, mz, tx, ty, tz) / startDist) * 100)
local vec = vector(tx - mx, ty - my, tz - mz)
vec:normalize()
mx = mx + vec.x * step; my = my + vec.y * step; mz = mz + vec.z * step
sendVehicle(mx, my, mz)
-- the sync is unreliable: a burst of hundreds of packets in one frame gets partly dropped and the server rolls back to the last
-- position it got ("a bit, again a bit"). A short yield every few packets keeps them arriving in order.
sent = sent + 1
if sent % BURST == 0 then wait(0) end
end
if not teleport.boolean then return false end -- stopped by hand
for _ = 1, 3 do sendVehicle(tx, ty, tz) end
sampAddChatMessage('TP: ' .. os.time() - time .. 's', -1)
if fakeEnd then
fakeStart(tx, ty, tz, true)
else
requestCollision(tx, ty)
loadScene(tx, ty, tz)
-- ground height: probe from above (the old probe from tz+5 returned the floor below when tz was under the ground)
local gz = 0
local t0 = os.clock()
while gz == 0 and os.clock() - t0 < 1.5 do
gz = getGroundZFor3dCoord(tx, ty, tz + 60)
if gz == 0 then gz = getGroundZFor3dCoord(tx, ty, 800.0) end
if gz == 0 then requestCollision(tx, ty); wait(50) end
end
local landZ = (gz ~= 0) and (gz + 0.8) or (tz + 3.0)
setCarCoordinates(car, tx, ty, landZ)
wait(100)
-- still under the map: lift it
local _, _, cz0 = getCarCoordinates(car)
if cz0 < landZ - 2 then setCarCoordinates(car, tx, ty, landZ + 2.0) end
-- landed in water: look around for dry ground (rings of 8 points), up to 20 tries
if isCarInWater(car) then
local tries = 0
for r = 6, 60, 6 do
for a = 0, 7 do
local ang = math.rad(a * 45)
local nx, ny = tx + math.cos(ang) * r, ty + math.sin(ang) * r
requestCollision(nx, ny)
local ngz = getGroundZFor3dCoord(nx, ny, tz + 30)
if ngz ~= 0 then
setCarCoordinates(car, nx, ny, ngz + 0.5)
wait(80)
tries = tries + 1
if not isCarInWater(car) then break end
end
end
if not isCarInWater(car) or tries >= 20 then break end
end
end
-- the char jumped out of the car in the water: back to the driver seat
if not isCharInAnyCar(PLAYER_PED) then
warpCharIntoCar(PLAYER_PED, car)
wait(100)
end
end
teleport.boolean = false
teleport.packet = 0
if not fakeEnd then
-- push the real position to the server right away, a few times: its streamer (cars, pickups, players) works from the last known position
for _ = 1, 5 do
if carDriving() then sampForceVehicleSync() else sampForceOnfootSync() end
wait(60)
end
end
return true
end
function teleportToBlip(vehicleId, mx, my, mz, cx, cy, cz, fakeEnd)
local step = ini.catch.rtpStep
local vehicleHandle = select(2, sampGetCarHandleBySampVehicleId(tonumber(vehicleId)))
local start_distance = getDistanceBetweenCoords3d(mx, my, mz, cx, cy, cz)
local time = os.time()
sampSendEnterVehicle(vehicleId, true)
while getDistanceBetweenCoords3d(mx, my, mz, cx, cy, cz) >= step and teleport.boolean do
teleport.persent = math.floor(100 - (getDistanceBetweenCoords3d(mx, my, mz, cx, cy, cz) / start_distance) * 100)
local vec = vector(cx - mx, cy - my, cz - mz)
vec:normalize()
mx = mx + vec.x * step
my = my + vec.y * step
mz = mz + vec.z * step
sendPassenger(mx, my, mz, vehicleId)
wait(0)
end
sendPassenger(cx, cy, cz, vehicleId)
if fakeEnd then
-- the local ped stays where it is: the server gets onfoot sync at the destination
fakeStart(cx, cy, cz)
teleport.boolean = false
teleport.packet = 0
sampAddChatMessage('TP (fake): ' .. os.time() - time .. 's', -1)
return
end
sampAddChatMessage('TP: ' .. os.time() - time .. 's', -1)
requestCollision(cx, cy)
wait(200)
local groundZ = getGroundZFor3dCoord(cx, cy, cz + 5)
if groundZ == 0 then
wait(500)
groundZ = getGroundZFor3dCoord(cx, cy, cz + 5)
end
if groundZ == 0 then groundZ = cz end
setCharCoordinates(PLAYER_PED, cx, cy, groundZ + 1.0)
setCarCoordinates(vehicleHandle, cx + 3, cy, groundZ + 0.5)
teleport.boolean = false
teleport.packet = 0
end
function infobar()
local font = renderCreateFont('Comic Sans MS', 10, 4)
local realtorFont = renderCreateFont('Arial', 9, 5)
local lastFrame = os.clock()
while true do wait(0)
local nowF = os.clock()
setCharCanBeKnockedOffBike(PLAYER_PED, false) -- the char cannot fall off a bike
if nowF - lastFrame > 0.7 then
sampAddChatMessage(string.format('{ff6969}[stall] game froze %.1fs, last call: %s (%.1fs before)', nowF - lastFrame, lastTpCall, (lastFrame - lastTpCallAt)), -1)
end
lastFrame = nowF
-- hunt stopped / ended while the fake position is on: bring the local ped to it, real sync again
if fake.on and not apartment.hunting and not teleport.boolean then
fakeStop(true)
end
-- watchdog: the teleport flag cannot stay on for more than 2 minutes (it blocks the sync)
if teleport.boolean then
tpSince = tpSince or os.clock()
if os.clock() - tpSince > 120 then
teleport.boolean = false
tpSince = nil
sampAddChatMessage('{ff6969}[r-realtor] TP flag stuck, reset', -1)
end
else
tpSince = nil
end
local statusText = ''
if apartment.hunting then
statusText = apartment.target and ('{69ff69}HUNT: #' .. apartment.target.aptNumber) or '{ffff69}HUNTING...'
elseif catch.active then
statusText = '{ffff69}' .. catch.status
elseif teleport.boolean then
statusText = '{ffff69}TP ' .. teleport.persent .. '%'
else
local s = '{ff6969}N/A'
if isCharOnFoot(PLAYER_PED) then
local v = getVehicle(200, true, false)
if v then s = '{69ff69}OK (id:' .. v .. ')' end
end
statusText = '{FFFFFF}' .. s
end
if teleport.bar_move then
if sampGetCursorMode() ~= 2 then sampSetCursorMode(2) end
local px, py = getCursorPos()
if isKeyJustPressed(0x01) then
sampSetCursorMode(0); ini.main.barX, ini.main.barY = px, py; teleport.bar_move = false; SaveCfg()
elseif isKeyJustPressed(0x02) then
sampSetCursorMode(0); teleport.bar_move = false
end
renderFontDrawText(font, rainbowText('r-realtor by @purpurins', 1) .. '\n' .. statusText, px, py, -1)
elseif ini.main.bar then
renderFontDrawText(font, rainbowText('r-realtor by @purpurins', 1) .. '\n' .. statusText, ini.main.barX, ini.main.barY, -1)
end
-- r-realtor by @purpurins HUD
if ini.main.bar then
local huntColor = apartment.hunting and '{69FF69}' or '{FF6969}'
local huntText = apartment.hunting and 'ON' or 'OFF'
local paydayColor = ini.catch.autoPayday and '{69FF69}' or '{FF6969}'
local paydayText = ini.catch.autoPayday and 'ON' or 'OFF'
local aptText = 'NONE'
local aptColor = '{FF6969}'
if apartment.target then
aptText = '#' .. apartment.target.aptNumber .. ' Ent#' .. apartment.target.entrance
aptColor = '{69FF69}'
end
local stText = 'IDLE'
local stColor = '{FFFFFF}'
if apartment.waitingBuy then
stText = 'BUYING'
stColor = '{69FF69}'
elseif apartment.waitingElevator then
stText = 'ELEVATOR'
stColor = '{FFFF69}'
elseif apartment.hunting and apartment.target then
stText = 'CATCHING'
stColor = '{FFFF69}'
elseif apartment.hunting then
stText = 'SEARCHING'
stColor = '{FFFF69}'
end
local realtorHud = '{FFFFFF}r-realtor by @purpurins\n'
.. '{FFFFFF}HUNTING: ' .. huntColor .. '[' .. huntText .. ']\n'
.. '{FFFFFF}PAYDAY: ' .. paydayColor .. '[' .. paydayText .. ']\n'
.. '{FFFFFF}APT: ' .. aptColor .. '[' .. aptText .. ']\n'
.. '{FFFFFF}STATUS: ' .. stColor .. '[' .. stText .. ']\n'
.. '{FFFFFF}LIMIT: {FFFF69}[' .. ini.catch.priceLimit .. ']\n'
.. '{FFFFFF}FREE: {FFFF69}[' .. countFreeApartments() .. ']\n'
.. '{FFFFFF}ORDER: {FFFF69}[#' .. ini.catch.apartmentIndex .. ']'
renderFontDrawText(realtorFont, realtorHud, ini.main.barX, ini.main.barY + 40, -1)
end
end
end
-- stepwise (curd) teleport from the current position to a point, then a real set at the end
function curdToPoint(x, y, z)
local px, py, pz = getCharCoordinates(PLAYER_PED)
teleport.boolean = true
guardFreeze()
teleportToCar(px, py, pz, x, y, z)
teleport.boolean = false
setCharCoordinates(PLAYER_PED, x, y, z)
end
function teleportToCar(mx, my, mz, cx, cy, cz)
local h = cz > mz
local step = ini.catch.curdStep
while getDistanceBetweenCoords3d(mx, my, mz, cx, cy, cz) >= step and teleport.boolean do
local vec = vector(cx - mx, cy - my, cz - mz)
vec:normalize()
mx = mx + vec.x * step; my = my + vec.y * step; mz = mz + vec.z * step
sendPlayer(mx, my, mz, h)
wait(0)
end
sendPlayer(cx, cy, cz, h)
end
function sendPassenger(x, y, z, vehicleId)
local vehicleHandle = select(2, sampGetCarHandleBySampVehicleId(tonumber(vehicleId)))
local rx, ry, rz, dx, dy, dz = getCarRealMatrix(vehicleHandle)
local u = createSync('unoccupied')
u.vehicleId = tonumber(vehicleId); u.seatId = 0
u.roll = {rx, ry, rz}; u.direction = {dx, dy, dz}
u.vehicleHealth = getCarHealth(vehicleHandle); u.position = {x, y, z}; u.send()
teleport.packet = teleport.packet + 1
if teleport.packet == 10 then
local p = createSync('passenger')
p.vehicleId = tonumber(vehicleId); p.seatId = 1
p.health = getCharHealth(PLAYER_PED); p.armor = getCharArmour(PLAYER_PED)
p.position = {x, y, z}; p.send()
teleport.packet = 0
end
sendPlayer(x, y, z, true, vehicleId)
end
function sendPlayer(x, y, z, h, surfingVehicleId)
local p = createSync('player')
if tonumber(surfingVehicleId) then
p.surfingOffsets = {1, 1, 1}; p.surfingVehicleId = tonumber(surfingVehicleId)
end
p.moveSpeed = {0.1, 0.1, h and 0.1 or -0.1}; p.position = {x, y, z}; p.send()
end
-- only the sync packets are held back while teleporting (not everything: a stuck flag used to freeze the player for good)
local SYNC_PACKETS = {[200] = true, [203] = true, [204] = true, [206] = true, [207] = true, [209] = true, [210] = true, [211] = true, [212] = true}
function onSendPacket(id, bs, priority, reliability, orderingChannel)
if (teleport.boolean or fake.on) and SYNC_PACKETS[id] then return false end
end
function createSync(sync_type, copy_from_player)
copy_from_player = (copy_from_player == nil) and true or copy_from_player
local sync_traits = {
player = {'PlayerSyncData', raknet.PACKET.PLAYER_SYNC, sampStorePlayerOnfootData},
passenger = {'PassengerSyncData', raknet.PACKET.PASSENGER_SYNC, sampStorePlayerPassengerData},
vehicle = {'VehicleSyncData', raknet.PACKET.VEHICLE_SYNC, sampStorePlayerIncarData},
unoccupied = {'UnoccupiedSyncData', raknet.PACKET.UNOCCUPIED_SYNC, nil}
}
local si = sync_traits[sync_type]
local dt = 'struct ' .. si[1]
local data = ffi.new(dt, {})
local ptr = tonumber(ffi.cast('uintptr_t', ffi.new(dt .. '*', data)))
if copy_from_player then
local cf = si[3]
if cf then
local pid = (copy_from_player == true) and select(2, sampGetPlayerIdByCharHandle(PLAYER_PED)) or tonumber(copy_from_player)
cf(pid, ptr)
end
end
local function func_send()
local bs = raknetNewBitStream()
raknetBitStreamWriteInt8(bs, si[2])
raknetBitStreamWriteBuffer(bs, ptr, ffi.sizeof(data))
raknetSendBitStreamEx(bs, sampfuncs.HIGH_PRIORITY, sampfuncs.UNRELIABLE_SEQUENCED, 1)
raknetDeleteBitStream(bs)
end
return setmetatable({send = func_send}, {
__index = function(t, k) return data[k] end,
__newindex = function(t, k, v) data[k] = v end
})
end
-- mark pointer: resolved by signature in clientside.dll, static offset is the fallback
local MARK_SIG = '8B 0D ? ? ? ? 8D 45 F0 50 E8 11' -- mov ecx,[mark_slot] in clientside.dll (from mark_dump)
local MARK_SIG_DISP = 2 -- byte index of the 4-byte absolute address inside the pattern
local MARK_OFFSET_FALLBACK = 0x445FD0 -- RVA of the mark slot (was 0x43C350), used only if the signature is not found
local markPtrAddr = nil
local function findSig(mh, sig)
local bytes, mask = {}, {}
for tok in sig:gmatch('%S+') do
if tok == '?' or tok == '??' then bytes[#bytes + 1] = 0; mask[#mask + 1] = false
else bytes[#bytes + 1] = tonumber(tok, 16); mask[#mask + 1] = true end
end
local n = #bytes
if n == 0 then return nil end
local nt = ffi.cast('uint32_t*', mh + 0x3C)[0]
local codeSize = ffi.cast('uint32_t*', mh + nt + 0x1C)[0] -- SizeOfCode
local codeBase = ffi.cast('uint32_t*', mh + nt + 0x2C)[0] -- BaseOfCode
local base = ffi.cast('uint8_t*', mh + codeBase)
for i = 0, codeSize - n do
local ok = true
for j = 1, n do
if mask[j] and base[i + j - 1] ~= bytes[j] then ok = false; break end
end
if ok then return mh + codeBase + i end
end
return nil
end
local function resolveMarkPtrAddr(mh)
if markPtrAddr then return markPtrAddr end
local hit = findSig(mh, MARK_SIG)
if hit then
markPtrAddr = ffi.cast('uint32_t*', hit + MARK_SIG_DISP)[0]
return markPtrAddr
end
if MARK_SIG ~= '' then
sampAddChatMessage('{ff6969}[r-realtor by @purpurins] mark signature not found, using old offset', -1)
end
markPtrAddr = mh + MARK_OFFSET_FALLBACK
return markPtrAddr
end
function getMark()
local mh = getModuleHandle("clientside.dll")
if mh == 0 then return false end
local ptr = memory.getint32(resolveMarkPtrAddr(mh), true)
if ptr == 0 then return false end
local tx = memory.getfloat(ptr + 0x10, true)
local ty = memory.getfloat(ptr + 0x14, true)
if tx == 0.0 and ty == 0.0 then return false end
local tz = getGroundZFor3dCoord(tx, ty, 1000.0)
return true, tx, ty, (tz == 0 and 15.0 or tz)
end
-- set position only after collision/scene at the target is loaded (interiors: no floor yet = fall into the void)
function safeSetPos(x, y, z, timeoutSec)
requestCollision(x, y)
loadScene(x, y, z)
local deadline = os.clock() + (timeoutSec or 3.0)
while os.clock() < deadline do
local gz = getGroundZFor3dCoord(x, y, z + 1.0)
if gz ~= 0 then break end
requestCollision(x, y)
wait(50)
end
if isCharInAnyCar(PLAYER_PED) then
warpCharFromCarToCoord(PLAYER_PED, x, y, z)
else
setCharCoordinates(PLAYER_PED, x, y, z)
end
end
function getCheckPoint()
local rx, ry, rz = 0.0, 0.0, 0.0
local found = false
for id = 0, 31 do
local ms = 0xC7F168 + id * 56
local mx = representIntAsFloat(readMemory(ms + 0, 4, false))
local my = representIntAsFloat(readMemory(ms + 4, 4, false))
local mz = representIntAsFloat(readMemory(ms + 8, 4, false))
if mx ~= 0.0 or my ~= 0.0 or mz ~= 0.0 then rx, ry, rz = mx, my, mz; found = true end
end
return found, rx, ry, rz
end
function getCarRealMatrix(handle)
local entity = getCarPointer(handle)
if entity ~= 0 then
local cm = memory.getuint32(entity + 20, true)
if cm ~= 0 then
return memory.getfloat(cm+0,true), memory.getfloat(cm+4,true), memory.getfloat(cm+8,true),
memory.getfloat(cm+16,true), memory.getfloat(cm+20,true), memory.getfloat(cm+24,true)
end
end
end
function getVehicle(checkDist, passenger, driver)
local dist, id = 9999, -1
local px, py, pz = getCharCoordinates(PLAYER_PED)
for _, veh in pairs(getAllVehicles()) do
local exist, vid = sampGetVehicleIdByCarHandle(veh)
if exist then
local dc = (getDriverOfCar(veh) ~= -1)
if (not driver and not dc) or (driver and dc) then
local cx, cy, cz = getCarCoordinates(veh)
local nd = getDistanceBetweenCoords3d(cx, cy, cz, px, py, pz)
if (passenger and getMaximumNumberOfPassengers(veh) > 0) or not passenger then
if nd < checkDist and dist > nd then id = vid; dist = nd end
end
end
end
end
return id ~= -1 and id or false
end
function rainbowText(text, speed)
local r = {}
local len = #text
for i = 1, len do
local c = text:sub(i, i)
local rv = math.floor(math.sin((os.clock() + (1.00/len)*i) * speed + 0) * 127 + 128)
local gv = math.floor(math.sin((os.clock() + (1.00/len)*i) * speed + 2) * 127 + 128)
local bv = math.floor(math.sin((os.clock() + (1.00/len)*i) * speed + 4) * 127 + 128)
r[#r+1] = string.format('{%02X%02X%02X}%s', rv, gv, bv, c)
end
return table.concat(r)
end