Biome rework (#37735)
* DungeonData rework Back to fields, serializes better, just make new layers dumby. * wawawewa * Fix this * Fixes * Port the work over * wawawewa * zoom * Kinda workin * Adjust wawa * Unloading work * Ore + entitytable fixes Iterate every dungeon not just last. * Big shot * wawawewa * Fixes * true * Fixes # Conflicts: # Content.Server/Procedural/DungeonJob/DungeonJob.cs * wawawewa * Fixes * Fix * Lot of work * wawawewa * Fixing * eh? * a * Fix a heap of stuff * Better ignored check * Reserve tile changes * biome * changes * wawawewa * Fixes & snow * Shadow fixes * wawawewa * smol * Add layer API * More work * wawawewa * Preloads and running again * wawawewa * Modified * Replacements and command * Runtime support * werk * Fix expeds + dungeon alltiles * reh --------- Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
This commit is contained in:
12
Content.Shared/Procedural/DungeonLayers/AutoCablingDunGen.cs
Normal file
12
Content.Shared/Procedural/DungeonLayers/AutoCablingDunGen.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Runs cables throughout the dungeon.
|
||||
/// </summary>
|
||||
public sealed partial class AutoCablingDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Entity;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Iterates room edges and places the relevant tiles and walls on any free indices.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dungeon data keys are:
|
||||
/// - CornerWalls (Optional)
|
||||
/// - FallbackTile
|
||||
/// - Walls
|
||||
/// </remarks>
|
||||
public sealed partial class BoundaryWallDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField]
|
||||
public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms;
|
||||
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Wall;
|
||||
|
||||
[DataField]
|
||||
public EntProtoId? CornerWall;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum BoundaryWallFlags : byte
|
||||
{
|
||||
Rooms = 1 << 0,
|
||||
Corridors = 1 << 1,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns entities inside corners.
|
||||
/// </summary>
|
||||
public sealed partial class CornerClutterDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField]
|
||||
public float Chance = 0.50f;
|
||||
|
||||
[DataField(required:true)]
|
||||
public ProtoId<EntityTablePrototype> Contents = new();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Adds entities randomly to the corridors.
|
||||
/// </summary>
|
||||
public sealed partial class CorridorClutterDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField]
|
||||
public float Chance = 0.05f;
|
||||
|
||||
/// <summary>
|
||||
/// The default starting bulbs
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Applies decal skirting to corridors.
|
||||
/// </summary>
|
||||
public sealed partial class CorridorDecalSkirtingDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Decal where 1 edge is found.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<DirectionFlag, string> CardinalDecals = new();
|
||||
|
||||
/// <summary>
|
||||
/// Decal where 1 corner edge is found.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<Direction, string> PocketDecals = new();
|
||||
|
||||
/// <summary>
|
||||
/// Decal where 2 or 3 edges are found.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<DirectionFlag, string> CornerDecals = new();
|
||||
|
||||
/// <summary>
|
||||
/// Optional color to apply to the decals.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color? Color;
|
||||
}
|
||||
28
Content.Shared/Procedural/DungeonLayers/CorridorDunGen.cs
Normal file
28
Content.Shared/Procedural/DungeonLayers/CorridorDunGen.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Connects room entrances via corridor segments.
|
||||
/// </summary>
|
||||
public sealed partial class CorridorDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// How far we're allowed to generate a corridor before calling it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Given the heavy weightings this needs to be fairly large for larger dungeons.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public int PathLimit = 2048;
|
||||
|
||||
/// <summary>
|
||||
/// How wide to make the corridor.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Width = 3f;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Selects [count] rooms and places external doors to them.
|
||||
/// </summary>
|
||||
public sealed partial class DungeonEntranceDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// How many rooms we place doors on.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Count = 1;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns entities on either side of an entrance.
|
||||
/// </summary>
|
||||
public sealed partial class EntranceFlankDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents = new();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// If external areas are found will try to generate windows.
|
||||
/// </summary>
|
||||
public sealed partial class ExternalWindowDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Procedural.Distance;
|
||||
using Robust.Shared.Noise;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.DungeonLayers;
|
||||
@@ -6,10 +9,6 @@ namespace Content.Shared.Procedural.DungeonLayers;
|
||||
/// <summary>
|
||||
/// Fills unreserved tiles with the specified entity prototype.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// DungeonData keys are:
|
||||
/// - Fill
|
||||
/// </remarks>
|
||||
public sealed partial class FillGridDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
@@ -20,4 +19,29 @@ public sealed partial class FillGridDunGen : IDunGenLayer
|
||||
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Entity;
|
||||
|
||||
#region Noise
|
||||
|
||||
[DataField]
|
||||
public bool Invert;
|
||||
|
||||
/// <summary>
|
||||
/// Optionally don't spawn entities if the noise value matches.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FastNoiseLite? ReservedNoise;
|
||||
|
||||
/// <summary>
|
||||
/// Noise threshold for <see cref="ReservedNoise"/>. Does nothing without it.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Threshold = -1f;
|
||||
|
||||
[DataField]
|
||||
public IDunGenDistance? DistanceConfig;
|
||||
|
||||
[DataField]
|
||||
public Vector2 Size;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// If internal areas are found will try to generate windows.
|
||||
/// </summary>
|
||||
public sealed partial class InternalWindowDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
}
|
||||
24
Content.Shared/Procedural/DungeonLayers/JunctionDunGen.cs
Normal file
24
Content.Shared/Procedural/DungeonLayers/JunctionDunGen.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Places the specified entities at junction areas.
|
||||
/// </summary>
|
||||
public sealed partial class JunctionDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Width to check for junctions.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Width = 3;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Places the specified entities on the middle connections between rooms
|
||||
/// </summary>
|
||||
public sealed partial class MiddleConnectionDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// How much overlap there needs to be between 2 rooms exactly.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int OverlapCount = -1;
|
||||
|
||||
/// <summary>
|
||||
/// How many connections to spawn between rooms.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Count = 1;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<EntityTablePrototype>? Flank;
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.DungeonLayers;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Spawns mobs inside of the dungeon randomly.
|
||||
/// </summary>
|
||||
|
||||
15
Content.Shared/Procedural/DungeonLayers/RoofDunGen.cs
Normal file
15
Content.Shared/Procedural/DungeonLayers/RoofDunGen.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Robust.Shared.Noise;
|
||||
|
||||
namespace Content.Shared.Procedural.DungeonLayers;
|
||||
|
||||
/// <summary>
|
||||
/// Sets tiles as rooved.
|
||||
/// </summary>
|
||||
public sealed partial class RoofDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField]
|
||||
public float Threshold = -1f;
|
||||
|
||||
[DataField]
|
||||
public FastNoiseLite? Noise;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Places tiles / entities onto room entrances.
|
||||
/// </summary>
|
||||
public sealed partial class RoomEntranceDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
}
|
||||
36
Content.Shared/Procedural/DungeonLayers/SampleDecalDunGen.cs
Normal file
36
Content.Shared/Procedural/DungeonLayers/SampleDecalDunGen.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Noise;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Shared.Procedural.DungeonLayers;
|
||||
|
||||
public sealed partial class SampleDecalDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserve any tiles we update.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ReserveTiles = true;
|
||||
|
||||
[DataField(customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
|
||||
public List<string> AllowedTiles { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Divide each tile up by this amount.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Divisions = 1f;
|
||||
|
||||
[DataField]
|
||||
public FastNoiseLite Noise { get; private set; } = new(0);
|
||||
|
||||
[DataField]
|
||||
public float Threshold { get; private set; } = 0.8f;
|
||||
|
||||
[DataField] public bool Invert { get; private set; } = false;
|
||||
|
||||
[DataField(required: true)]
|
||||
public List<ProtoId<DecalPrototype>> Decals = new();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Noise;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Shared.Procedural.DungeonLayers;
|
||||
|
||||
/// <summary>
|
||||
/// Samples noise to spawn the specified entity
|
||||
/// </summary>
|
||||
public sealed partial class SampleEntityDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserve any tiles we update.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ReserveTiles = true;
|
||||
|
||||
[DataField(customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
|
||||
public List<string> AllowedTiles { get; private set; } = new();
|
||||
|
||||
[DataField] public FastNoiseLite Noise { get; private set; } = new(0);
|
||||
|
||||
[DataField]
|
||||
public float Threshold { get; private set; } = 0.5f;
|
||||
|
||||
[DataField] public bool Invert { get; private set; } = false;
|
||||
|
||||
[DataField]
|
||||
public List<EntProtoId> Entities = new();
|
||||
}
|
||||
35
Content.Shared/Procedural/DungeonLayers/SampleTileDunGen.cs
Normal file
35
Content.Shared/Procedural/DungeonLayers/SampleTileDunGen.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Noise;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Procedural.DungeonLayers;
|
||||
|
||||
/// <summary>
|
||||
/// Samples noise and spawns the specified tile in the dungeon area.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class SampleTileDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserve any tiles we update.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ReserveTiles = true;
|
||||
|
||||
[DataField] public FastNoiseLite Noise { get; private set; } = new(0);
|
||||
|
||||
[DataField]
|
||||
public float Threshold { get; private set; } = 0.5f;
|
||||
|
||||
[DataField] public bool Invert { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<byte>? Variants = null;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.DungeonLayers;
|
||||
|
||||
/// <summary>
|
||||
/// Connects dungeons via points that get subdivided.
|
||||
/// </summary>
|
||||
public sealed partial class SplineDungeonConnectorDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<ContentTileDefinition>? WidenTile;
|
||||
|
||||
/// <summary>
|
||||
/// Will divide the distance between the start and end points so that no subdivision is more than these metres away.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int DivisionDistance = 20;
|
||||
|
||||
/// <summary>
|
||||
/// How much each subdivision can vary from the middle.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float VarianceMax = 0.15f;
|
||||
}
|
||||
24
Content.Shared/Procedural/DungeonLayers/WallMountDunGen.cs
Normal file
24
Content.Shared/Procedural/DungeonLayers/WallMountDunGen.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns on the boundary tiles of rooms.
|
||||
/// </summary>
|
||||
public sealed partial class WallMountDunGen : IDunGenLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Chance per free tile to spawn a wallmount.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public double Prob = 0.1;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<EntityTablePrototype> Contents;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Procedural.PostGeneration;
|
||||
|
||||
// Ime a worm
|
||||
/// <summary>
|
||||
/// Generates worm corridors.
|
||||
/// </summary>
|
||||
public sealed partial class WormCorridorDunGen : IDunGenLayer
|
||||
{
|
||||
[DataField]
|
||||
public int PathLimit = 2048;
|
||||
|
||||
/// <summary>
|
||||
/// How many times to run the worm
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Count = 20;
|
||||
|
||||
/// <summary>
|
||||
/// How long to make each worm
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Length = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount the angle can change in a single step.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Angle MaxAngleChange = Angle.FromDegrees(45);
|
||||
|
||||
/// <summary>
|
||||
/// How wide to make the corridor.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Width = 3f;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile;
|
||||
}
|
||||
Reference in New Issue
Block a user