Download src/server/Main.server.luau from maddiedreese/lamplight: direct link, hf CLI and curl.
- Browser
- Download file 14.7 kB
-
https://huggingface.co/maddiedreese/lamplight/resolve/main/src/server/Main.server.luau
- Command line
-
hf download hf://maddiedreese/lamplight/src/server/Main.server.luau
-
curl -L -o Main.server.luau https://huggingface.co/maddiedreese/lamplight/resolve/main/src/server/Main.server.luau
14.7 kB
| local Players=game:GetService('Players') | |
| local RunService=game:GetService('RunService') | |
| local RS=game:GetService('ReplicatedStorage') | |
| local SS=game:GetService('ServerStorage') | |
| local TextService=game:GetService('TextService') | |
| local TextChatService=game:GetService('TextChatService') | |
| local Safety=require(script.Parent.ChatSafety).new(TextService,TextChatService) | |
| local Config=require(RS.Shared.Config) | |
| local Tokenizer=require(script.Parent.Tokenizer) | |
| local Engine=require(script.Parent.Inference) | |
| local remote=Instance.new('RemoteEvent');remote.Name='Club';remote.Parent=RS | |
| local terminals=workspace:WaitForChild('Lab'):WaitForChild('Terminals') | |
| local rooms={};local ready=false;local loading=0;local tokenizer | |
| local jobs={};local waiting={};local rate={} | |
| local inputChannel=Instance.new('TextChannel');inputChannel.Name='LamplightInput';inputChannel.Parent=TextChatService | |
| local displayChannel=Instance.new('TextChannel');displayChannel.Name='LamplightDisplay';displayChannel.Parent=TextChatService | |
| local function fire(player,kind,data) if player and player.Parent then remote:FireClient(player,kind,data) end end | |
| local function status(room,text) | |
| room.status=text;room.model:SetAttribute('Status',text);room.model:SetAttribute('Busy',room.busy) | |
| if room.owner then fire(room.owner,'status',{status=text,terminal=room.id}) end | |
| end | |
| local function sendView(room,recipient) | |
| local revision=room.viewRevision;local owner=room.owner;local history=table.clone(room.displayHistory) | |
| task.spawn(function() | |
| local allowed=owner and Safety:canView(owner.UserId,recipient.UserId) | |
| if room.viewRevision~=revision or room.owner~=owner then return end | |
| fire(recipient,'view',{terminal=room.id,revision=revision,history=if allowed then history else {}}) | |
| end) | |
| end | |
| local function broadcast(room) | |
| room.viewRevision=(room.viewRevision or 0)+1 | |
| room.model:SetAttribute('Transcript',nil) | |
| room.model:SetAttribute('RequestVersion',room.version) | |
| room.model:SetAttribute('Busy',room.busy) | |
| room.model:SetAttribute('Owner',if room.owner then room.owner.DisplayName else '') | |
| room.model:SetAttribute('OwnerId',if room.owner then room.owner.UserId else 0) | |
| room.model:SetAttribute('Status',room.status) | |
| local pp=room.model.Interact.ProximityPrompt | |
| pp.ActionText=if room.owner then 'In use' else 'Sit at terminal' | |
| for _,recipient in Players:GetPlayers() do sendView(room,recipient) end | |
| end | |
| local function near(player,room,distance) | |
| local root=player.Character and player.Character:FindFirstChild('HumanoidRootPart') | |
| return root and (root.Position-room.model.Interact.Position).Magnitude<=distance | |
| end | |
| local function cancel(room) | |
| room.version+=1;room.busy=false | |
| end | |
| local function release(room) | |
| if room.releasing then return end | |
| room.releasing=true | |
| local owner=room.owner | |
| local character=owner and owner.Character | |
| local humanoid=character and character:FindFirstChildOfClass('Humanoid') | |
| local wasSeated=humanoid and humanoid.SeatPart==room.seat | |
| local exitCFrame=if wasSeated then room.seatHome*CFrame.new(0,2,5) else nil | |
| cancel(room);room.owner=nil;room.history={};room.displayHistory={};room.publicPrompt=nil;room.publicAnswer=nil | |
| -- Remove the seat-to-avatar weld before moving the avatar, preserving the chair assembly. | |
| local weld=room.seat:FindFirstChild('SeatWeld');if weld then weld:Destroy() end | |
| room.seat.Disabled=true | |
| if wasSeated then | |
| humanoid.Sit=false | |
| humanoid:ChangeState(Enum.HumanoidStateType.GettingUp) | |
| task.spawn(function() | |
| -- Let the seat detach before moving the avatar; never teleport the chair's assembly. | |
| RunService.Heartbeat:Wait();RunService.Heartbeat:Wait() | |
| local root=character:FindFirstChild('HumanoidRootPart') | |
| if root and not humanoid.SeatPart and not room.seat:FindFirstChild('SeatWeld') then | |
| root.CFrame=exitCFrame | |
| root.AssemblyLinearVelocity=Vector3.zero;root.AssemblyAngularVelocity=Vector3.zero | |
| end | |
| end) | |
| end | |
| status(room,if ready then 'READY' else 'WARMING UP');broadcast(room);fire(owner,'released',{terminal=room.id}) | |
| task.delay(1,function() room.releasing=false;room.seat.Disabled=false end) | |
| end | |
| local function claim(player,room) | |
| if room.releasing then return end | |
| if not near(player,room,Config.ClaimDistance) then return end | |
| local humanoid=player.Character and player.Character:FindFirstChildOfClass('Humanoid') | |
| if not humanoid or humanoid.Health<=0 then return end | |
| if room.seat.Occupant and room.seat.Occupant~=humanoid then return end | |
| if humanoid.SeatPart and humanoid.SeatPart~=room.seat then return end | |
| if room.owner and room.owner~=player then | |
| fire(player,'notice',{text='This terminal is in use by '..room.owner.DisplayName..'. You can read its public monitor.'});return | |
| end | |
| for _,other in rooms do if other~=room and other.owner==player then release(other) end end | |
| room.owner=player;room.lastActive=os.clock();broadcast(room) | |
| if humanoid.SeatPart~=room.seat then room.seat:Sit(humanoid) end | |
| task.delay(.5,function() | |
| if room.owner==player and room.seat.Occupant~=humanoid then release(room) end | |
| end) | |
| fire(player,'open',{terminal=room.id,status=room.status,ready=ready,busy=room.busy}) | |
| end | |
| for i=1,4 do | |
| local model=terminals:WaitForChild('Terminal'..i) | |
| local room={id=i,model=model,seat=model:WaitForChild('Chair seat'),status='WARMING UP',history={},displayHistory={},version=0,busy=false,lastActive=0} | |
| room.seatHome=room.seat.CFrame | |
| rooms[i]=room | |
| model.Interact.ProximityPrompt.Triggered:Connect(function(player) claim(player,room) end) | |
| room.seat:GetPropertyChangedSignal('Occupant'):Connect(function() | |
| local humanoid=room.seat.Occupant | |
| if humanoid then | |
| local player=Players:GetPlayerFromCharacter(humanoid.Parent) | |
| if player and room.owner~=player then claim(player,room) end | |
| elseif room.owner and not room.releasing then release(room) end | |
| end) | |
| broadcast(room) | |
| end | |
| -- Only a completed TextChatService submission can create a model request. | |
| inputChannel.ShouldDeliverCallback=function(message,recipient) | |
| local source=message.TextSource | |
| if not source or recipient.UserId~=source.UserId then return false end | |
| local player=Players:GetPlayerByUserId(source.UserId) | |
| local id,version=message.Metadata:match('^Lamplight:(%d):(%d+):[%w%-]+$') | |
| id=tonumber(id);version=tonumber(version) | |
| local room=id and rooms[id] | |
| if not room or room.owner~=player or room.version~=version or room.busy or #message.Text>Config.MaxPromptCharacters*4 or not near(player,room,Config.ReleaseDistance) then return false end | |
| Safety:record(player.UserId,message.MessageId,id,room.version,message.Text,os.clock()) | |
| return true | |
| end | |
| local function addChatUser(player) | |
| task.spawn(function() | |
| local ok,source=pcall(function() return inputChannel:AddUserAsync(player.UserId) end) | |
| if not ok or not source then fire(player,'notice',{text='Roblox chat is unavailable for this account.'}) end | |
| end) | |
| end | |
| Players.PlayerAdded:Connect(addChatUser) | |
| for _,player in Players:GetPlayers() do addChatUser(player) end | |
| local function enqueue(player,room,text) | |
| if not ready then fire(player,'notice',{text='The model is warming up. Please wait a moment.'});return end | |
| if room.busy then return end | |
| if not player.Character or room.seat.Occupant~=player.Character:FindFirstChildOfClass('Humanoid') then return end | |
| if type(text)~='string' or #text>Config.MaxPromptCharacters*4 or not utf8.len(text) or utf8.len(text)>Config.MaxPromptCharacters or not text:match('%S') then return end | |
| room.busy=true;room.lastActive=os.clock();room.version+=1;local version=room.version | |
| status(room,'CHECKING MESSAGE') | |
| task.spawn(function() | |
| local allowed=Safety:canView(player.UserId,player.UserId) | |
| local safe,reason | |
| if allowed then safe,reason=Safety:filter(text,player.UserId) end | |
| if room.version~=version or room.owner~=player then return end | |
| if not safe then room.busy=false;status(room,'MESSAGE WITHHELD');broadcast(room);fire(player,'notice',{terminal=room.id,text=if reason=='professional' then Safety.professionalNotice elseif reason=='blocked' then 'This message was blocked by Roblox filtering. Please try a different message.' else 'Roblox chat or filtering is unavailable. Your message was not sent.'});return end | |
| local messages={{role='system',content=Config.SystemPrompt}} | |
| for _,m in room.history do table.insert(messages,m) end | |
| table.insert(messages,{role='user',content=safe}) | |
| local ids=tokenizer:chat(messages) | |
| while #ids>Config.MaxContext-Config.MaxOutput and #messages>2 do table.remove(messages,2);if #messages>2 then table.remove(messages,2) end;ids=tokenizer:chat(messages) end | |
| if #ids>Config.MaxContext-Config.MaxOutput then room.busy=false;status(room,'MESSAGE TOO LONG');broadcast(room);fire(player,'notice',{text='That message uses too many tokens. Please shorten it.'});return end | |
| room.publicPrompt=safe;room.publicAnswer='';table.insert(room.displayHistory,{role='user',content=safe}) | |
| fire(player,'accepted',{text=safe});status(room,'WAITING FOR A TURN');broadcast(room) | |
| table.insert(waiting,{room=room,player=player,version=version,messages=messages,ids=ids,index=1,output={},rng=Random.new(),started=os.clock(),generated=0}) | |
| end) | |
| end | |
| remote.OnServerEvent:Connect(function(player,action,data) | |
| if type(action)~='string' or type(data)~='table' then return end | |
| if action=='hello' then fire(player,'welcome',{ready=ready,loading=loading});for _,room in rooms do sendView(room,player) end;return end | |
| local id=data.terminal;if type(id)~='number' or id%1~=0 then return end | |
| local room=rooms[id];if not room then return end | |
| if action=='claim' then claim(player,room);return end | |
| if room.owner~=player then return end | |
| if action=='release' then release(room);room.displayHistory={};return end | |
| if not near(player,room,Config.ReleaseDistance) then release(room);return end | |
| room.lastActive=os.clock() | |
| if action=='stop' then cancel(room);status(room,'STOPPED');broadcast(room);fire(player,'done',{cancelled=true});return end | |
| if action=='new' then cancel(room);room.history={};room.displayHistory={};room.publicPrompt=nil;room.publicAnswer=nil;status(room,'READY');broadcast(room);fire(player,'cleared',{});return end | |
| if action=='ask' then | |
| local text=Safety:take(player.UserId,data.messageId,id,room.version,os.clock()) | |
| if not text then fire(player,'notice',{terminal=id,text='Your message was not accepted by Roblox chat. Please send it again.'});return end | |
| local now=os.clock();if now-(rate[player.UserId] or 0)<2 then fire(player,'notice',{text='Please wait a moment before sending another message.'});return end;rate[player.UserId]=now;enqueue(player,room,text) | |
| end | |
| end) | |
| Players.PlayerRemoving:Connect(function(player) | |
| for _,room in rooms do if room.owner==player then release(room);room.displayHistory={} end end | |
| rate[player.UserId]=nil;Safety.receipts[player.UserId]=nil | |
| end) | |
| task.spawn(function() | |
| while true do | |
| task.wait(5) | |
| for _,room in rooms do | |
| if room.owner and (not near(room.owner,room,Config.ReleaseDistance) or (not room.busy and os.clock()-room.lastActive>Config.IdleSeconds)) then release(room);room.displayHistory={} elseif room.owner then for _,recipient in Players:GetPlayers() do sendView(room,recipient) end end | |
| end | |
| end | |
| end) | |
| local function valid(job) return job.room.version==job.version and job.room.owner==job.player and job.player.Parent~=nil end | |
| local function finish(job) | |
| local room=job.room | |
| status(room,'PREPARING PUBLIC RESPONSE') | |
| task.spawn(function() | |
| local raw=tokenizer:decode(job.output) | |
| -- A byte-level tokenizer may stop in the middle of a UTF-8 code point. | |
| while #raw>0 and not utf8.len(raw) do raw=raw:sub(1,-2) end | |
| local safe,reason=Safety:filter(raw,job.player.UserId,true) | |
| if not valid(job) then return end | |
| if not safe then safe=if reason=='professional' then Safety.professionalNotice elseif reason=='blocked' then 'This response was blocked by Roblox filtering. Please try a different message.' else 'The response could not be checked by Roblox filtering and was not displayed. Please try again.' end | |
| room.publicAnswer=safe;room.history=job.messages;table.remove(room.history,1);table.insert(room.history,{role='assistant',content=safe}) | |
| table.insert(room.displayHistory,{role='assistant',content=safe});while #room.displayHistory>12 do table.remove(room.displayHistory,1) end | |
| room.busy=false;room.lastActive=os.clock();status(room,'READY');broadcast(room) | |
| fire(job.player,'done',{tokens=#job.output,seconds=os.clock()-job.started}) | |
| print(string.format('[Lamplight] Terminal %d: %d tokens in %.2fs',room.id,#job.output,os.clock()-job.started)) | |
| end) | |
| end | |
| task.spawn(function() | |
| local ok,err=pcall(function() | |
| tokenizer=Tokenizer.new(SS:WaitForChild('TokenizerData')) | |
| Engine.load(SS:WaitForChild('ModelData'),function(p) | |
| loading=p;RS:SetAttribute('ModelLoading',p) | |
| end) | |
| end) | |
| if not ok then warn('[Lamplight] Model load failed: '..tostring(err));for _,r in rooms do status(r,'MODEL UNAVAILABLE');broadcast(r) end;return end | |
| ready=true;RS:SetAttribute('ModelReady',true) | |
| for _,r in rooms do status(r,'READY');broadcast(r) end | |
| remote:FireAllClients('ready',{}) | |
| print('[Lamplight] SmolLM2 ready. Pure Luau. '..Engine.manifest.weightBytes..' weight bytes.') | |
| while true do | |
| while #jobs<Config.ActiveSessions and #waiting>0 do | |
| local j=table.remove(waiting,1) | |
| if valid(j) then j.session=Engine.newSession(Config.MaxContext);table.insert(jobs,j) end | |
| end | |
| if #jobs==0 then task.wait(.1) else | |
| for index=#jobs,1,-1 do | |
| local j=jobs[index] | |
| if not valid(j) then table.remove(jobs,index) else | |
| local okStep,errorStep=pcall(function() | |
| local prefill=j.index<=#j.ids | |
| local id=if prefill then j.ids[j.index] else j.output[#j.output] | |
| local projected=not prefill or j.index==#j.ids | |
| local logits=Engine.forward(j.session,id,projected,function() return not valid(j) end) | |
| j.index+=1 | |
| if projected then | |
| local nextId=Engine.sample(logits,Config.Temperature,j.rng,j.output) | |
| if nextId==2 or j.session.position>=Config.MaxContext-1 then j.finished=true else table.insert(j.output,nextId) end | |
| if #j.output>=Config.MaxOutput then j.finished=true end | |
| status(j.room,'WRITING · '..#j.output..' TOKENS') | |
| else status(j.room,'READING · '..(j.index-1)..' / '..#j.ids) end | |
| local display=j.room.model.Screen.TerminalDisplay;display.State.Text=j.room.status | |
| if os.clock()-j.started>240 then error('Response time limit reached') end | |
| end) | |
| if not okStep then | |
| table.remove(jobs,index) | |
| if valid(j) then j.room.busy=false;status(j.room,'TRY AGAIN');fire(j.player,'done',{text='The model took too long or encountered an error. Try a shorter message.'});warn('[Lamplight] '..tostring(errorStep)) end | |
| elseif j.finished then table.remove(jobs,index);finish(j) end | |
| end | |
| end | |
| end | |
| end | |
| end) | |