RestaurantServiceExpansion.cs
using Sandbox;
using System;
using System.Threading.Tasks;
namespace PaintballBaddies;
/// <summary>The delivery bay uses the restaurant's real exterior doorway.
/// Mounted workshop art remains a dependency; our source edits and dressing
/// are kept separately so the original workshop map is never overwritten.</summary>
public sealed partial class RestaurantServiceExpansion : Component
{
public const string DeliveryVanPackage="rust.vana";
public const string WorldModel="maps/last_orders_service/restaurant_world.vmdl";
public bool Ready {get;private set;}
public string Error {get;private set;}="";
public bool GeometryReady {get;private set;}
private GameObject root;
private GameObject sourceWorld,sourcePhysics;
private MapInstance sourceMap;
private bool started;
protected override void OnUpdate()
{
var venue=Scene.GetAllComponents<RestaurantArena>().FirstOrDefault();
if(started || ArenaSelection.Current(Scene)!=ArenaSelection.Restaurant
|| Scene.GetAllComponents<MapInstance>().Any(m=>m.Active && !m.IsLoaded)
|| venue?.Ready!=true || !venue.SourcePrepared)return;
started=true;_=Build();
}
private async Task Build()
{
try
{
var map=Scene.GetAllComponents<MapInstance>().FirstOrDefault();
if(!map.IsValid())throw new InvalidOperationException("Restaurant map is missing");
var world=Model.Load(WorldModel);
if(world is null || world.IsError)throw new InvalidOperationException("Edited restaurant geometry did not compile");
// The original mounted package still supplies its props, lights and
// materials. Replace only static architecture, with matching cut
// render/collision meshes. A failure leaves the old map playable.
root=new GameObject(true,"Last Orders delivery bay"){NetworkMode=NetworkMode.Never};
var architecture=Place("Restaurant service architecture",world,Vector3.Zero,0,1,false,false);
architecture.Tags.Add("workshop_geometry");
sourceWorld=map.GameObject.Children.FirstOrDefault(g=>g.Name=="worldspawn");
sourcePhysics=map.GameObject.Children.FirstOrDefault(g=>g.Components.Get<MapCollider>().IsValid());
if(!sourceWorld.IsValid() || !sourcePhysics.IsValid())throw new InvalidOperationException("Original restaurant architecture could not be identified");
sourceMap=map;
// Native world rendering belongs to MapInstance, not worldspawn's
// placeholder MapObjectComponent. Retain loaded props/lights, then
// unload the original world through the public engine lifecycle.
// The edited model is already present before old collision leaves.
foreach(var child in map.GameObject.Children.ToArray())
if(child!=sourceWorld && child!=sourcePhysics)
child.SetParent(child.Network.Active ? null : root,true);
sourceMap.Enabled=false;
GeometryReady=true;
var upperFinish=new GameObject(root,true,"Diner upper wall finish"){NetworkMode=NetworkMode.Never};
upperFinish.Components.Create<ModelRenderer>().Model=Model.Load("models/restaurant/service/diner_upper_finish.vmdl");
foreach(var renderer in Scene.GetAllComponents<ModelRenderer>().Where(r=>r.GameObject.Name.StartsWith("Restaurant finish ")))
{
string name=renderer.GameObject.Name.Substring("Restaurant finish ".Length);
if(name=="kitchen_back_wall" || name.StartsWith("diner_walls_"))renderer.Model=Model.Load($"models/restaurant/service/{name}.vmdl");
}
Frame(460,1728,1808,142,52);
Frame(752,1096,1190,146,42);
var van=await Cloud.Load<Model>(DeliveryVanPackage);
await Cloud.Load<Model>("facepunch.fence_panel_large");
await Cloud.Load<Model>("facepunch.fence_panel_gate");
if(!IsValid || Scene!=Game.ActiveScene)return;
if(van is null || van.IsError)throw new InvalidOperationException("Delivery van did not mount");
Place("Concrete loading ramp","models/restaurant/service/loading_landing.vmdl",Vector3.Zero,0,1,false,false);
Place("Restaurant delivery van",van,new Vector3(1080,1145,32),0,1,true,true);
Place("Delivery pallet","models/foundry/foundry_pallet/foundry_pallet.vmdl",new Vector3(885,1005,32),0,1.1f,true,false);
Place("Delivery produce crate","models/citizen_props/crate01.vmdl",new Vector3(885,1005,39),0,1,true,true);
Place("Empty return crate","models/citizen_props/crate01.vmdl",new Vector3(1070,1322,32),18,.82f,true,true);
Place("Loading bay bollard south","models/sbox_props/parking_bollard/parking_bollard.vmdl",new Vector3(792,1025,32),0,1,true,false);
Place("Loading bay bollard north","models/sbox_props/parking_bollard/parking_bollard.vmdl",new Vector3(792,1264,32),0,1,true,false);
Sign("DELIVERIES",new Vector3(772,1143,172),180,.58f);
Sign("NO RETURNS. THEY BITE.",new Vector3(772,1143,156),180,.23f);
RefreshDeliveryDetails();
await DressServiceAlley();
Scene.NavMesh.SetDirty();Ready=true;Error="";
Scene.GetAllComponents<KitchenDoorways>().FirstOrDefault()?.Refresh();
}
catch(Exception ex)
{
Error="Delivery bay unavailable: "+ex.Message;Log.Warning("RESTAURANT_SERVICE "+Error);
if(!GeometryReady && root.IsValid())root.Destroy();
Ready=GeometryReady;
Scene.NavMesh.SetDirty();
Scene.GetAllComponents<KitchenDoorways>().FirstOrDefault()?.Refresh();
}
}
public void RefreshDeliveryDetails()
{
if(!root.IsValid())return;
foreach(var go in root.Children.Where(g=>g.Name=="Delivery notice"))
{
var label=go.Components.Get<TextRenderer>();bool large=label.Text=="DELIVERIES";
go.WorldPosition=new Vector3(772,1143,large ? 172 : 156);go.WorldRotation=Rotation.FromYaw(180);label.Scale=large ? .58f : .23f;
}
foreach(var name in new[]{"delivery_yard","delivery_brick_wall"})
{
if(!root.Children.Any(g=>g.Name==name))Place(name,"models/restaurant/service/"+name+".vmdl",Vector3.Zero,0,1,false,false);
}
root.Children.First(g=>g.Name=="delivery_yard").Components.Get<ModelRenderer>().MaterialOverride=Material.Load("materials/industrial_arena/floor.vmat");
// The original map's baked carpark lamps have enormous HDR values.
// The new dynamic world must not receive their unbaked floodlighting.
foreach(var light in root.Components.GetAll<PointLight>(FindMode.EverythingInSelfAndDescendants).Where(l=>l.WorldPosition.x>730 && l.GameObject.Name.StartsWith("light_omni")))light.Enabled=false;
foreach(var light in root.Components.GetAll<SpotLight>(FindMode.EverythingInSelfAndDescendants).Where(l=>l.WorldPosition.x>730))light.Enabled=false;
foreach(var sun in root.Components.GetAll<DirectionalLight>(FindMode.EverythingInSelfAndDescendants).Where(l=>l.GameObject.Name=="light_environment"))sun.LightColor=new Color(.33f,.27f,.21f);
if(!root.Children.Any(g=>g.Name=="Delivery doorway warm light"))
{
var lamp=new GameObject(root,true,"Delivery doorway warm light"){NetworkMode=NetworkMode.Never,WorldPosition=new Vector3(798,1143,179)};
var light=lamp.Components.Create<PointLight>();light.LightColor=new Color(1,.79f,.5f)*.65f;light.Radius=330;light.Shadows=true;
}
var warm=root.Children.First(g=>g.Name=="Delivery doorway warm light").Components.Get<PointLight>();warm.LightColor=new Color(1,.79f,.5f)*.65f;
if(!root.Children.Any(g=>g.Name.StartsWith("Delivery perimeter fence")))
{
var fence=Model.Load("models/props/temporary_fencing/fence_panel_large.vmdl");
Fence(fence,new Vector3(769,850,32.16f),new Vector3(1370,850,32.16f),4);
Fence(fence,new Vector3(1370,850,32.16f),new Vector3(1370,1123,32.16f),2);
// Paired hinged leaves give the van a believable 23-foot delivery
// entrance. Closed during combat, with matching solid collision.
var gate=Model.Load("models/props/temporary_fencing/fence_panel_gate.vmdl");
Fence(gate,new Vector3(1370,1123,32.16f),new Vector3(1370,1397,32.16f),2);
Fence(fence,new Vector3(1370,1397,32.16f),new Vector3(1370,1670,32.16f),2);
Fence(fence,new Vector3(1370,1670,32.16f),new Vector3(769,1670,32.16f),4);
}
}
private void Fence(Model model,Vector3 start,Vector3 end,int count)
{
var direction=(end-start).Normal;float length=(end-start).Length/count;float yaw=Rotation.LookAt(direction).Yaw();
for(int i=0;i<count;i++)Place("Delivery perimeter fence "+start+" "+i,model,start+direction*(length*(i+.5f)),yaw,length/model.Bounds.Size.x,true,false);
var go=new GameObject(root,false,"Delivery perimeter boundary"){NetworkMode=NetworkMode.Never,WorldPosition=(start+end)*.5f,WorldRotation=Rotation.LookAt(direction)};
var collision=go.Components.Create<BoxCollider>();collision.Center=Vector3.Up*46;collision.Scale=new Vector3((end-start).Length,5,92);collision.Static=true;go.Enabled=true;
}
private GameObject Place(string name,string path,Vector3 at,float yaw,float scale,bool alignFloor,bool cover)
=>Place(name,Model.Load(path),at,yaw,scale,alignFloor,cover);
private GameObject Place(string name,Model model,Vector3 at,float yaw,float scale,bool alignFloor,bool cover)
{
if(model is null || model.IsError)throw new InvalidOperationException(name+" model is missing");
var rotation=Rotation.FromYaw(yaw);
var go=new GameObject(root,false,name){NetworkMode=NetworkMode.Never,WorldPosition=alignFloor ? at-rotation*(model.Bounds.Center.WithZ(model.Bounds.Mins.z)*scale) : at,WorldRotation=rotation,WorldScale=new Vector3(scale)};
go.Components.Create<ModelRenderer>().Model=model;
var collider=go.Components.Create<ModelCollider>();collider.Model=model;collider.Static=true;
if(cover){var c=go.Components.Create<CoverSurface>();c.ModelHeight=model.Bounds.Maxs.z;c.StandingOnly=model.Bounds.Size.z*scale>65;}
go.Enabled=true;return go;
}
private void Sign(string text,Vector3 at,float yaw,float scale)
{
var go=new GameObject(root,true,"Delivery notice"){NetworkMode=NetworkMode.Never,WorldPosition=at,WorldRotation=Rotation.FromYaw(yaw)};
var label=go.Components.Create<TextRenderer>();label.Text=text;label.FontFamily="Arial";label.FontWeight=800;label.FontSize=36;label.Scale=scale;
label.Color=new Color(1,.85f,.58f);label.HorizontalAlignment=TextRenderer.HAlignment.Center;label.VerticalAlignment=TextRenderer.VAlignment.Center;
}
private void Frame(float x,float y0,float y1,float top,float depth)
{
// Full-depth jambs close the sectioned wall edges, with real collision.
// Their inner faces stay at the aperture boundary, preserving its width.
foreach(var spec in new[]{
(At:new Vector3(x,y0-2.5f,(top+32)*.5f),Size:new Vector3(depth,5,top-32)),
(At:new Vector3(x,y1+2.5f,(top+32)*.5f),Size:new Vector3(depth,5,top-32)),
(At:new Vector3(x,(y0+y1)*.5f,top+3),Size:new Vector3(depth,y1-y0+10,6))})
{
var model=Model.Load("models/dev/box.vmdl");var go=new GameObject(root,false,"Service doorway steel jamb"){NetworkMode=NetworkMode.Never};
go.WorldScale=spec.Size/model.Bounds.Size;go.WorldPosition=spec.At-model.Bounds.Center*go.WorldScale;
var render=go.Components.Create<ModelRenderer>();render.Model=model;render.MaterialOverride=Material.Load("materials/foundry/metalplates006/metalplates006.vmat");render.Tint=new Color(.28f,.3f,.31f);
var collider=go.Components.Create<ModelCollider>();collider.Model=model;collider.Static=true;go.Enabled=true;
}
}
protected override void OnDestroy()
{
if(root.IsValid())root.Destroy();
}
}