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();

    }
}