-- FilmKit Access 0.1.2 | Personal server loader local function accessModule() -- Licensing protocol state machine. No executor installation or hardware collection. local Client={}; Client.__index=Client local function validToken(v) return type(v)=="string" and #v==43 and v:match("^[%w_-]+$")~=nil end local function validId(v) return type(v)=="string" and v:match("^%d+$") and #v<=20 end local credentialErrors={TOKEN_REQUIRED=true,DEVICE_CREDENTIAL_INVALID=true,DEVICE_REVOKED=true,LICENSE_UNAVAILABLE=true,ACCOUNT_MISMATCH=true,ACCOUNT_OR_DEVICE_MISMATCH=true,SESSION_UNAVAILABLE=true} function Client.normalizeKey(value) if type(value)~="string" or #value>512 then return end local key=value:gsub("%s","") for _,space in ipairs({"\194\160","\226\128\175","\226\128\139","\226\128\140","\226\128\141","\226\129\160","\239\187\191"}) do key=key:gsub(space,"") end for _,dash in ipairs({"‐","‑","‒","–","—","―","−","﹣","-"}) do key=key:gsub(dash,"-") end key=key:gsub("^[`\"']+",""):gsub("[`\"']+$","") for _,quote in ipairs({"«","»","“","”"}) do key=key:gsub("^"..quote,""):gsub(quote.."$","") end if #key==46 and key:match("^FK%-[A-Za-z0-9_-]+$") then return key end end function Client.new(d) assert(d.call and d.clock and d.userId and d.installationId and d.baseUrl) return setmetatable({d=d,closed=false,deadline=0},Client) end function Client:authorized() return not self.closed and self.session~=nil and self.d.clock()180 then return false,"INVALID_SESSION" end self.session=value.sessionToken; self.deadline=self.d.clock()+value.leaseSeconds; self.pair=nil; self.reason=nil self.d.save({server=self.d.baseUrl,userId=self.d.userId,installationId=self.d.installationId,refreshToken=value.refreshToken}) return true end function Client:resume() local v=self.d.load() local serverKnown=type(v)=="table" and v.server==self.d.baseUrl if type(v)=="table" then for _,alias in ipairs(self.d.serverAliases or {}) do if v.server==alias then serverKnown=true end end end if type(v)~="table" or not serverKnown or v.userId~=self.d.userId or v.installationId~=self.d.installationId or not validToken(v.refreshToken) then return false,"NO_SAVED_SESSION" end local r,code,status=self.d.call("POST","/api/v1/session/resume",{userId=self.d.userId,installationId=self.d.installationId},v.refreshToken) if not r then if (status==401 or status==403) and credentialErrors[code] then self.d.save({}) end; return false,code end return self:accept(r) end function Client:begin(key,name) if self.closed then return false,"CLOSED" end self.pair=nil; key=Client.normalizeKey(key) if not key then return false,"INVALID_KEY" end local r,code=self.d.call("POST","/api/v1/pair/start",{key=key,deviceName=name,userId=self.d.userId,installationId=self.d.installationId}) if not r then return false,code end if not validToken(r.pairToken) or type(r.code)~="string" or not r.code:match("^%x+$") or #r.code~=12 or r.verificationUrl~=self.d.baseUrl.."/activate?code="..r.code then return false,"INVALID_PAIR" end self.pair={token=r.pairToken,code=r.code,url=r.verificationUrl,deadline=self.d.clock()+600} return true,self.pair end function Client:poll() if self.closed or not self.pair or self.d.clock()>self.pair.deadline then return false,"PAIR_EXPIRED" end local r,code=self.d.call("POST","/api/v1/pair/poll",{},self.pair.token) if not r then return false,code end if r.status=="pending" then return nil,"PENDING" end if r.status~="ready" then return false,"INVALID_PAIR" end return self:accept(r) end function Client:heartbeat() if not self:authorized() then self:check(); return false,self.reason or "NO_SESSION" end local r,code,status=self.d.call("POST","/api/v1/session/heartbeat",{userId=self.d.userId,installationId=self.d.installationId},self.session) if not r then if (status==401 or status==403) and credentialErrors[code] then self.d.save({}); self:stop(code) else self:check() end return false,code end if type(r.leaseSeconds)~="number" or r.leaseSeconds~=r.leaseSeconds or r.leaseSeconds<15 or r.leaseSeconds>180 then self:stop("INVALID_SESSION"); return false,"INVALID_SESSION" end if self.closed then return false,"CLOSED" end self.deadline=self.d.clock()+r.leaseSeconds; return true end function Client:check() if not self.closed and self.session and self.d.clock()>=self.deadline then self:stop("LEASE_EXPIRED") end return self:authorized() end function Client:stop(reason) if self.closed then return end self.closed=true; self.reason=reason or "STOPPED"; self.session=nil; self.pair=nil if self.d.onStop then self.d.onStop(self.reason) end end return Client end local AccessClient=accessModule() local function transportModule() -- HTTP response adapters only. Never log request bodies, credentials or response bodies. local Transport={} function Transport.new(d) return function(method,path,value,auth,binary,limit) local headers={["User-Agent"]="FilmKit/0.1.2"}; if auth then headers.Authorization="Bearer "..auth end if value then headers["Content-Type"]="application/json" end local ok,response=pcall(d.request,{Url=d.baseUrl..path,Method=method,Headers=headers,Body=value and d.encode(value) or nil}) local status=0; local body local function fail(code) if d.report then pcall(d.report,{code=code,status=status,path=path,bodyType=type(body),bodyBytes=type(body)=="string" and #body or 0}) end return nil,code,status end if not ok or type(response)~="table" then return fail("NETWORK_UNAVAILABLE") end status=tonumber(response.StatusCode or response.statusCode or response.status_code or response.Status or response.status) or 0 body=response.Body or response.body or response.responseBody if status<100 or status>599 then return fail("INVALID_RESPONSE") end if type(body)~="string" or #body==0 then return fail(status>=400 and "HTTP_FILTERED" or "INVALID_RESPONSE") end if #body>(limit or 16000) then return fail("RESPONSE_TOO_LARGE") end if body:sub(1,3)=="\239\187\191" then body=body:sub(4) end local decoded if not binary then local good,v=pcall(d.decode,body); if good then decoded=v end end local code=type(decoded)=="table" and type(decoded.error)=="string" and #decoded.error<=64 and decoded.error:match("^[A-Z0-9_]+$") and decoded.error if status<200 or status>=300 then return fail(code or (status==429 and "RATE_LIMITED" or status>=500 and "SERVICE_UNAVAILABLE" or "HTTP_FILTERED")) end if binary then return body end if type(decoded)~="table" then return fail("INVALID_RESPONSE") end if code then return fail(code) end return decoded end end return Transport end local AccessTransport=transportModule() -- FilmKit personal access loader. Generated endpoint contains no server secrets. local BASE="https://filmkit-access.filmkit-skrypnic2006.workers.dev" local H=game:GetService("HttpService"); local P=game:GetService("Players"); local R=game:GetService("RunService"); local U=game:GetService("UserInputService") assert(R:IsClient() and P.LocalPlayer,"Откройте плейс в Roblox Player") local requestFn=request or http_request or (syn and syn.request) assert(type(requestFn)=="function" and type(loadstring)=="function","Среда не предоставляет HTTP request / loadstring") assert(type(writefile)=="function" and type(readfile)=="function","Для ассетов и сессии нужна файловая поддержка среды") local env=type(getgenv)=="function" and getgenv() or _G if env.__FilmKitAccess and env.__FilmKitAccess.stop then pcall(env.__FilmKitAccess.stop,"RESTARTED") end pcall(function() if makefolder then makefolder("RobloxFilmKit"); makefolder("RobloxFilmKit/Assets") end end) local function readJson(path) local ok,text=pcall(readfile,path); if not ok or type(text)~="string" or #text>12000 then return end local good,value=pcall(H.JSONDecode,H,text); return good and type(value)=="table" and value or nil end local function writeJson(path,value) return pcall(writefile,path,H:JSONEncode(value)) end local install=readJson("RobloxFilmKit/Installation.json") if not install or type(install.id)~="string" or #install.id~=36 or not install.id:match("^[%x%-]+$") then install={id=H:GenerateGUID(false)}; writeJson("RobloxFilmKit/Installation.json",install) end local call=AccessTransport.new({request=requestFn,baseUrl=BASE,encode=function(v) return H:JSONEncode(v) end,decode=function(v) return H:JSONDecode(v) end, report=function(v) v.at=os.time(); writeJson("RobloxFilmKit/AccessLastError.json",v) end}) local messages={NO_SAVED_SESSION="Введите личный ключ.",KEY_OR_ACCOUNT_INVALID="Ключ недействителен или выдан другому Roblox UserId.",ROBLOX_SETUP_REQUIRED="Владелец ещё не настроил вход Roblox на сервере.",DEVICE_LIMIT="Достигнут лимит установок. Попросите владельца отозвать старую.",SESSION_LIMIT="Достигнут лимит активных запусков. Закройте старую сессию или попросите владельца отключить её.",PAIR_DENIED_OR_USED="Подключение отклонено или код использован. Начните заново.",PAIR_UNAVAILABLE="Код истёк. Начните подключение заново.",PAIR_EXPIRED="Код истёк. Введите ключ ещё раз.",DEVICE_REVOKED="Доступ этой установки отозван владельцем.",LICENSE_UNAVAILABLE="Ключ отозван или срок истёк.",NETWORK_UNAVAILABLE="Сервер недоступен. На бесплатном плане первый ответ после простоя может занять около минуты.",LEASE_EXPIRED="Связь с сервером не восстановлена. FilmKit остановлен.",INVALID_KEY="Нужен полный ключ вида FK-…",RATE_LIMITED="Слишком много запросов. Подождите перед повтором."} local player=P.LocalPlayer; local parent=player:WaitForChild("PlayerGui",15); assert(parent,"PlayerGui недоступен") local gui=Instance.new("ScreenGui"); gui.Name="FilmKitAccess"; gui.ResetOnSpawn=false; gui.IgnoreGuiInset=true; gui.DisplayOrder=200; gui.Parent=parent local frame=Instance.new("Frame"); frame.Size=UDim2.fromOffset(520,430); frame.Position=UDim2.new(.5,-260,.5,-215); frame.BackgroundColor3=Color3.fromRGB(20,29,25); frame.Parent=gui local corner=Instance.new("UICorner"); corner.CornerRadius=UDim.new(0,12); corner.Parent=frame local scale=Instance.new("UIScale"); scale.Parent=frame local function text(class,name,caption,y,height) local n=Instance.new(class); n.Name=name; n.Text=caption; n.TextColor3=Color3.fromRGB(231,246,237); n.Font=Enum.Font.Gotham; n.TextSize=14; n.TextWrapped=true n.Position=UDim2.fromOffset(20,y); n.Size=UDim2.new(1,-40,0,height); n.BackgroundTransparency=1; n.Parent=frame; return n end local title=text("TextButton","Title","FILMKIT · ЛИЧНЫЙ ДОСТУП",10,34) local close=text("TextButton","Close","×",10,32); close.Size=UDim2.fromOffset(32,32); close.Position=UDim2.new(1,-40,0,10) local info=text("TextLabel","Account","Roblox UserId: "..tostring(player.UserId).." · "..player.Name,49,34) local key=text("TextBox","Key","",90,38); key.PlaceholderText="Личный ключ FK-…"; key.BackgroundTransparency=0; key.BackgroundColor3=Color3.fromRGB(34,47,39); key.ClearTextOnFocus=false local device=text("TextBox","Device","Мой ПК",136,34); device.PlaceholderText="Имя этой установки"; device.BackgroundTransparency=0; device.BackgroundColor3=key.BackgroundColor3; device.ClearTextOnFocus=false local start=text("TextButton","Connect","Подключить / повторить",180,38); start.BackgroundTransparency=0; start.BackgroundColor3=Color3.fromRGB(40,100,65) start.Modal=true local codeLabel=text("TextLabel","Code","",224,36); codeLabel.Font=Enum.Font.Code; codeLabel.TextSize=24 local address=text("TextBox","Address","",265,55); address.TextEditable=false; address.ClearTextOnFocus=false; address.TextSize=12 local copy=text("TextButton","CopyURL","Скопировать адрес подтверждения",320,30) local statusLabel=text("TextLabel","Status","Проверяю сохранённую сессию…",356,62); statusLabel.TextSize=12 local function notice(value) statusLabel.Text=messages[value] or tostring(value) end messages.INVALID_RESPONSE="Не удалось распознать ответ HTTP. Подождите и повторите. Если ошибка остаётся, скачайте новый FilmKitStart.lua. Диагностика: AccessLastError.json." messages.SERVICE_UNAVAILABLE="Сервер временно не отвечает или просыпается. Подождите около минуты и нажмите «Подключить / повторить». Введённый ключ сохранён в поле." messages.HTTP_FILTERED="HTTP-ответ отклонён или пуст до проверки ключа. Это не означает, что ключ неверный. Используйте актуальный FilmKitStart.lua; диагностика: AccessLastError.json." messages.RESPONSE_TOO_LARGE="Ответ сервера больше допустимого размера. Скачайте новый загрузчик или сообщите владельцу." messages.INVALID_KEY="Нужен полный ключ: FK- и ещё 43 символа. Попросите владельца нажать «Скопировать полный ключ»." messages.KEY_OR_ACCOUNT_INVALID="Ключ отозван, истёк или выдан другому UserId. Владелец должен выдать ключ именно для "..tostring(player.UserId).."." local alive=true; local busy=true; local app; local core; local links={}; local drag start.Active=false local oldMouse={behavior=U.MouseBehavior,visible=U.MouseIconEnabled} local function killApp() if app and not app.dead then pcall(function() app:destroy() end) end end local function cleanup(reason) if not alive then return end; alive=false local raw=core and core.session if core then core:stop(reason) end; killApp() for _,c in ipairs(links) do c:Disconnect() end gui:Destroy(); U.MouseBehavior=oldMouse.behavior; U.MouseIconEnabled=oldMouse.visible if raw then task.spawn(function() call("POST","/api/v1/session/logout",{},raw) end) end end env.__FilmKitAccess={stop=cleanup} core=AccessClient.new({call=call,clock=os.clock,userId=tostring(player.UserId),installationId=install.id,baseUrl=BASE, serverAliases={"https://filmkit-access-skrypnic2006.onrender.com"}, load=function() return readJson("RobloxFilmKit/AccessSession.json") end, save=function(v) writeJson("RobloxFilmKit/AccessSession.json",v) end, onStop=function(reason) killApp(); if alive then frame.Visible=true; start.Active=false; notice(messages[reason] or ("Доступ завершён: "..reason..". Закройте окно и запустите загрузчик заново.")) end end}) local alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; local values={}; for i=1,64 do values[alphabet:sub(i,i)]=i-1 end local function base64(raw) assert(type(raw)=="string" and #raw%4==0 and not raw:find("[^%w+/=]"),"Неверные данные ассета") local out,parts={},{}; for i=1,#raw,4 do local a,b=values[raw:sub(i,i)],values[raw:sub(i+1,i+1)]; local c,d=values[raw:sub(i+2,i+2)],values[raw:sub(i+3,i+3)] assert(a and b,"Неверная кодировка ассета"); local n=a*262144+b*4096+(c or 0)*64+(d or 0) local x,y,z=math.floor(n/65536)%256,math.floor(n/256)%256,n%256 out[#out+1]=d and string.char(x,y,z) or c and string.char(x,y) or string.char(x) if #out>=4096 then parts[#parts+1]=table.concat(out); out={} end if i%65536==1 then task.wait() end end; parts[#parts+1]=table.concat(out); return table.concat(parts) end local assetNames={['Maw200.png']=true,['HeadTurn.wav']=true,['HeadTurnLoud220.wav']=true} local function runFilmKit() if not alive or not core:authorized() then return end notice("Доступ подтверждён. Проверяю ассеты…") local manifest,why=call("GET","/api/v1/manifest",nil,core.session); assert(manifest,why) assert(type(manifest.revision)=="string" and #manifest.revision==64 and manifest.revision:match("^%x+$"),"Неверная версия ассетов") local cached=readJson("RobloxFilmKit/AccessAssets.json"); local reuse=cached and cached.revision==manifest.revision assert(type(manifest.assets)=="table" and #manifest.assets==3,"Неверный список ассетов") for _,asset in ipairs(manifest.assets) do assert(assetNames[asset.name] and type(asset.bytes)=="number" and asset.bytes>0 and asset.bytes<8000000,"Неверный ассет") local path="RobloxFilmKit/Assets/"..asset.name; local exists,old=pcall(readfile,path); local existing=reuse and exists and type(old)=="string" and #old==asset.bytes if not existing then notice("Загружаю "..asset.name.."…") local encoded,err=call("GET","/api/v1/assets/"..asset.name.."?encoding=base64",nil,core.session,false,12000000); assert(encoded,err) local bytes=base64(encoded.data); assert(#bytes==asset.bytes,"Размер ассета не совпал") assert(core:check() and alive,"Сессия завершилась во время загрузки"); writefile(path,bytes) end end writeJson("RobloxFilmKit/AccessAssets.json",{revision=manifest.revision}) local code,reason=call("GET","/api/v1/bundle",nil,core.session,true,2500000); assert(code,reason) assert(core:check() and alive,"Доступ закончился до запуска"); assert(#code==manifest.bundleBytes and code:sub(1,100):find("RobloxFilmKit",1,true),"Сервер вернул не сборку FilmKit") local fn,message=loadstring(code); assert(fn,message); app=fn(); assert(app and not app.dead,"FilmKit не запустился") if not core:check() or not alive then killApp(); return end frame.Visible=false; key.Text=""; U.MouseBehavior=oldMouse.behavior; U.MouseIconEnabled=oldMouse.visible end local function loadAfterAuth() local ok,reason=pcall(runFilmKit); if not ok and alive then core:stop("LOAD_FAILED"); notice("Запуск не удался: "..tostring(reason):sub(1,220)) end end local lastBeat=os.clock();local beatBusy=false table.insert(links,R.Heartbeat:Connect(function() if not alive then return end core:check() if app and app.dead and not core.closed then cleanup("APP_STOPPED"); return end if core:authorized() and not beatBusy and os.clock()-lastBeat>=30 then lastBeat=os.clock(); beatBusy=true;task.spawn(function() core:heartbeat();beatBusy=false end) end if frame.Visible then U.MouseBehavior=Enum.MouseBehavior.Default; U.MouseIconEnabled=true end local camera=workspace.CurrentCamera; local v=camera and camera.ViewportSize or Vector2.new(1280,720); scale.Scale=math.min(1,(v.X-24)/520,(v.Y-24)/430) end)) table.insert(links,start.Activated:Connect(function() if busy or core.closed then return end; busy=true task.spawn(function() codeLabel.Text=""; address.Text="" local input=AccessClient.normalizeKey(key.Text) or key.Text; key.Text=input; notice("Подключаюсь к серверу…") local ok,p=core:begin(input,device.Text) if ok and alive then key.Text="" codeLabel.Text=p.code:sub(1,6).."-"..p.code:sub(7); address.Text=p.url; notice("Откройте адрес, войдите через Roblox и подтвердите код именно этой установки.") repeat task.wait(3); if not alive then break end; local ready,why=core:poll() if ready then loadAfterAuth(); break elseif ready==false then notice(why); break end until not alive elseif alive then notice(p) end busy=false end) end)) table.insert(links,copy.Activated:Connect(function() if type(setclipboard)=="function" and address.Text~="" then setclipboard(address.Text); notice("Адрес скопирован. Код сравните с окном браузера.") end end)) table.insert(links,close.Activated:Connect(function() cleanup("USER_CLOSED") end)) table.insert(links,title.InputBegan:Connect(function(input) if input.UserInputType==Enum.UserInputType.MouseButton1 then drag={start=U:GetMouseLocation(),position=frame.AbsolutePosition} end end)) table.insert(links,U.InputChanged:Connect(function(input) if drag and input.UserInputType==Enum.UserInputType.MouseMovement then local p=drag.position+U:GetMouseLocation()-drag.start; local v=workspace.CurrentCamera.ViewportSize; frame.Position=UDim2.fromOffset(math.clamp(p.X,0,math.max(0,v.X-520*scale.Scale)),math.clamp(p.Y,0,math.max(0,v.Y-430*scale.Scale))) end end)) table.insert(links,U.InputEnded:Connect(function(input) if input.UserInputType==Enum.UserInputType.MouseButton1 then drag=nil end end)) table.insert(links,U.WindowFocusReleased:Connect(function() drag=nil end)) task.spawn(function() local ok,why=core:resume(); if not alive then return end if ok then loadAfterAuth() else notice(why) end busy=false; start.Active=not core.closed end)