Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
b38e410
hacky but kinda working
amylizzle Feb 5, 2025
1abb28f
render target
amylizzle Feb 6, 2025
b697f68
fix offset
amylizzle Feb 6, 2025
6db9202
manager rendering
amylizzle Feb 17, 2025
0129924
Component system
amylizzle Feb 17, 2025
9138e9c
sandbox
amylizzle Feb 17, 2025
3d37193
TODO: stop taking on enormous projects when tired
amylizzle Feb 17, 2025
1334241
jesus christ this is so much shit
amylizzle Feb 18, 2025
82b6b86
generator
amylizzle Feb 18, 2025
b9b9142
more
amylizzle Feb 18, 2025
582f7d5
done! with this part.
amylizzle Feb 19, 2025
dda9e5e
closee
amylizzle Feb 19, 2025
2b51722
list, appearance, bugfixes
amylizzle Feb 19, 2025
a08e3b1
so close
amylizzle Feb 19, 2025
608cfc3
mark component as dirty
amylizzle Feb 19, 2025
0311905
I'm tired and it doesn't work and I don't know why
amylizzle Feb 19, 2025
ee38d34
split components bad
amylizzle Feb 20, 2025
47dc471
it *still* doesn't work and I *still* have no idea why
amylizzle Feb 20, 2025
32f9e1f
manual state
amylizzle Feb 20, 2025
2aebbc9
drawing
amylizzle Feb 20, 2025
572791b
whoops
amylizzle Feb 20, 2025
fb02985
working!!!!
amylizzle Feb 20, 2025
4f2e9a2
clean up generator
amylizzle Feb 20, 2025
58950e8
only one particle
amylizzle Feb 20, 2025
22c4f3d
Fix autogenerated component state stuff
amylizzle Feb 20, 2025
de1ba9f
friction and drift
amylizzle Feb 20, 2025
e5c568b
render above
amylizzle Feb 20, 2025
45e7f16
cleanup
amylizzle Feb 20, 2025
51638ab
Merge remote-tracking branch 'upstream/master' into particles_od
amylizzle Feb 20, 2025
dceeccd
Merge branch 'master' into particles_od
amylizzle Feb 20, 2025
007360d
viewport changes
amylizzle Feb 23, 2025
6587932
Merge branch 'master' into particles_od
amylizzle Feb 24, 2025
52226af
implement dists
amylizzle Feb 26, 2025
1a5db18
dist & type
amylizzle Mar 3, 2025
761e372
Merge branch 'master' into particles_od
amylizzle Mar 3, 2025
82f8865
replace these icons with license-y ones
amylizzle Mar 3, 2025
6cae859
goddamn cat
amylizzle Mar 3, 2025
901852d
Merge branch 'master' into particles_od
amylizzle Mar 7, 2025
61700b4
unfuck merge
amylizzle Mar 7, 2025
88e4001
Merge remote-tracking branch 'upstream/master' into particles_od
amylizzle Jun 17, 2025
308b72b
update RT to latest
amylizzle Jun 17, 2025
189bd3e
Merge remote-tracking branch 'upstream/master' into particles_od
amylizzle Jun 30, 2025
62ab448
fix merge
amylizzle Jun 30, 2025
42237e8
update RT
amylizzle Aug 11, 2025
e3eba5d
screw it, we don't need RT
amylizzle Aug 20, 2025
8d61dd9
Merge remote-tracking branch 'upstream/master' into particles_od
amylizzle Aug 20, 2025
3be3927
using
amylizzle Aug 20, 2025
28971ed
resharper part 1
amylizzle Aug 20, 2025
99f414e
Merge branch 'master' into particles_od
amylizzle Aug 21, 2025
e93e8a5
more resharper
amylizzle Aug 21, 2025
e96b00f
batch 3
amylizzle Aug 21, 2025
0c4dd8e
last ones
amylizzle Aug 21, 2025
fc4fd05
Merge remote-tracking branch 'upstream/master' into particles_od
amylizzle Aug 23, 2025
ff6de86
Merge branch 'master' into particles_od
amylizzle Aug 29, 2025
631f46f
get math'd nerd
amylizzle Sep 3, 2025
e0164d4
Merge branch 'master' into particles_od
amylizzle Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DMCompiler/DMStandard/Types/Atoms/Movable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//Undocumented var. "[x],[y]" or "[x],[y] to [x2],[y2]" based on bound_* vars
var/bounds as opendream_unimplemented

var/particles/particles as opendream_unimplemented
var/particles/particles

proc/Bump(atom/Obstacle)

Expand Down
48 changes: 24 additions & 24 deletions DMCompiler/DMStandard/Types/Particles.dm
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,34 @@
/particles
parent_type = /datum
//Particle vars that affect the entire set (generators are not allowed for these)
var/width = 100 as opendream_unimplemented //null defaults to 0. width is the size of the particle "image" ie particles within this width image will be rendered, if they are partially in they get partially cut. if they reenter this area after leaving it they reapper. image is centered on particle owner.
var/height = 100 as opendream_unimplemented //ditto
var/count = 100 as opendream_unimplemented // if null, uses the last set value. is checked BEFORE lifespan so (count 10, lifespan 10, spawning 1) will skip a pixel every 10 pixels
var/spawning = 1 as opendream_unimplemented // null is treated as 0
var/bound1 = -1000 as opendream_unimplemented // Usually list but if a number treated as list(bound1, bound1, bound1). if particles go above/below bound they will get immediately deleted regardless of lifespan. null is treated as the default value (-1000 and 1000)(this could be treated as infinity as well but 1000 is so large its hard to tell)
var/bound2 = 1000 as opendream_unimplemented // Ditto!
var/gravity as opendream_unimplemented // Usually list but if a number treated as list(gravity, gravity, gravity).
var/list/gradient = null as opendream_unimplemented // not cast as a list on byond as of 514.1580 despite only being able to be a list
var/transform as opendream_unimplemented // matrix or list. list can be simple matrix, complex matrix or projection matrix. thus: list(a, b, c, d, e, f) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz, cx,cy,cz) OR list(xx,xy,xz,xw, yx,yy,yz,yw, zx,zy,zz,zw, wx,wy,wz,ww)
var/width = 100 //null defaults to 0. width is the size of the particle "image" ie particles within this width image will be rendered, if they are partially in they get partially cut. if they reenter this area after leaving it they reapper. image is centered on particle owner.
var/height = 100 //ditto
var/count = 100 // if null, uses the last set value. is checked BEFORE lifespan so (count 10, lifespan 10, spawning 1) will skip a pixel every 10 pixels
var/spawning = 1 // null is treated as 0
var/bound1 = -1000 // Usually list but if a number treated as list(bound1, bound1, bound1). if particles go above/below bound they will get immediately deleted regardless of lifespan. null is treated as the default value (-1000 and 1000)(this could be treated as infinity as well but 1000 is so large its hard to tell)
var/bound2 = 1000 // Ditto!
var/gravity // Usually list but if a number treated as list(gravity, gravity, gravity).
var/list/gradient = null // not cast as a list on byond as of 514.1580 despite only being able to be a list
var/transform // matrix or list. list can be simple matrix, complex matrix or projection matrix. thus: list(a, b, c, d, e, f) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz, cx,cy,cz) OR list(xx,xy,xz,xw, yx,yy,yz,yw, zx,zy,zz,zw, wx,wy,wz,ww)

//Vars that apply when a particle spawns
var/lifespan as opendream_unimplemented // actual time a particle exists is fadein + lifespan + fade. thus this just the time it spends fully faded in. null is treated as
var/fade as opendream_unimplemented // null treated as 0
var/fadein as opendream_unimplemented // null treated as 0
var/icon as opendream_unimplemented // either icon or list(icon = weightofthisicon, icon = weightofthisicon) if null defaults to a 1x1 white pixel
var/icon_state as opendream_unimplemented // either string or list(string = weightofthisiconstate, string = weightofthisiconstate) if null defaults to a 1x1 white pixel
var/color as opendream_unimplemented // null treated as 0
var/color_change as opendream_unimplemented // null treated as 0
var/position as opendream_unimplemented // Usually list but if a number treated as list(position, position, position). null is treated as 0
var/velocity as opendream_unimplemented // Usually list but if a number treated as list(velocity, velocity, velocity). null is treated as 0
var/scale as opendream_unimplemented // if null defaults to 1, if number treated as list(scale, scale)
var/grow as opendream_unimplemented // if null defaults to 0, if number treated as list(grow, grow)
var/rotation as opendream_unimplemented // null treated as 0
var/spin as opendream_unimplemented // null treated as 0
var/friction as opendream_unimplemented // null treated as 0, numbers below 0 treated as 0
var/lifespan // actual time a particle exists is fadein + lifespan + fade. thus this just the time it spends fully faded in. null is treated as
var/fade // null treated as 0
var/fadein // null treated as 0
var/icon // either icon or list(icon = weightofthisicon, icon = weightofthisicon) if null defaults to a 1x1 white pixel
var/icon_state // either string or list(string = weightofthisiconstate, string = weightofthisiconstate) if null defaults to a 1x1 white pixel
var/color // null treated as 0
var/color_change // null treated as 0
var/position // Usually list but if a number treated as list(position, position, position). null is treated as 0
var/velocity // Usually list but if a number treated as list(velocity, velocity, velocity). null is treated as 0
var/scale // if null defaults to 1, if number treated as list(scale, scale)
var/grow // if null defaults to 0, if number treated as list(grow, grow)
var/rotation // null treated as 0
var/spin // null treated as 0
var/friction // null treated as 0, numbers below 0 treated as 0

//Vars that are evaluated every tick
var/drift as opendream_unimplemented // Usually list but if a number treated as list(drift, drift, drift)
var/drift // Usually list but if a number treated as list(drift, drift, drift)

//misc notes
// particle image height/width is not considered for TILE_BOUND-less atoms
Expand Down
2 changes: 0 additions & 2 deletions DMCompiler/DMStandard/UnsortedAdditions.dm
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
set opendream_unimplemented = TRUE
/proc/findtextEx_char(Haystack,Needle,Start=1,End=0)
set opendream_unimplemented = TRUE
/proc/generator(type, A, B, rand)
set opendream_unimplemented = TRUE
/proc/load_resource(File)
set opendream_unimplemented = TRUE
proc/missile(Type, Start, End)
Expand Down
1 change: 1 addition & 0 deletions DMCompiler/DMStandard/_Standard.dm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ proc/flist(Path) as /list
proc/floor(A) as num
proc/fract(n) as num
proc/ftime(File, IsCreationTime = 0) as num
proc/generator(type, A, B, rand) as /generator
proc/get_step_to(Ref, Trg, Min=0) as num
proc/get_steps_to(Ref, Trg, Min=0) as /list
proc/gradient(A, index)
Expand Down
2 changes: 2 additions & 0 deletions OpenDreamClient/ClientContentIoC.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using OpenDreamClient.Audio;
using OpenDreamClient.Interface;
using OpenDreamClient.Rendering.Particles;
using OpenDreamClient.Resources;
using OpenDreamClient.States;

Expand All @@ -11,5 +12,6 @@ public static void Register() {
IoCManager.Register<IDreamResourceManager, DreamResourceManager>();
IoCManager.Register<DreamUserInterfaceStateManager>();
IoCManager.Register<IDreamSoundEngine, DreamSoundEngine>();
IoCManager.Register<ParticlesManager>(); //TODO remove when particles RT PR is merged
}
}
6 changes: 5 additions & 1 deletion OpenDreamClient/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using OpenDreamClient.Audio;
using OpenDreamClient.Interface;
using OpenDreamClient.Rendering;
using OpenDreamClient.Rendering.Particles;
using OpenDreamClient.Resources;
using OpenDreamClient.States;
using OpenDreamShared;
Expand All @@ -23,6 +24,7 @@ public sealed class EntryPoint : GameClient {
[Dependency] private readonly ILightManager _lightManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly ParticlesManager _particleManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;

private const string IEUserAgent =
Expand Down Expand Up @@ -50,7 +52,7 @@ public override void Init() {

// This needs to happen after all IoC registrations, but before IoC.BuildGraph();
foreach (var callback in TestingCallbacks) {
var cast = (ClientModuleTestingCallbacks) callback;
var cast = (ClientModuleTestingCallbacks)callback;
cast.ClientBeforeIoC?.Invoke();
}

Expand All @@ -70,6 +72,7 @@ public override void Init() {
IoCManager.Resolve<ILocalizationManager>().LoadCulture(new CultureInfo("en-US"));

IoCManager.Resolve<IClyde>().SetWindowTitle("OpenDream");
_particleManager.Initialize(); //TODO remove when particles RT PR is merged
}

public override void PostInit() {
Expand All @@ -96,6 +99,7 @@ public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
switch (level) {
case ModUpdateLevel.FramePostEngine:
_dreamInterface.FrameUpdate(frameEventArgs);
_particleManager.FrameUpdate(frameEventArgs); //TODO remove when particles RT PR is merged
break;
case ModUpdateLevel.PostEngine:
break;
Expand Down
167 changes: 167 additions & 0 deletions OpenDreamClient/Rendering/ClientDreamParticlesSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using JetBrains.Annotations;
using OpenDreamClient.Interface;
using OpenDreamShared.Rendering;
using OpenDreamClient.Rendering.Particles;
using Robust.Client.Graphics;
using Robust.Shared.Random;
using Robust.Shared.Timing;

namespace OpenDreamClient.Rendering;

[UsedImplicitly]
public sealed class ClientDreamParticlesSystem : SharedDreamParticlesSystem
{
[Dependency] private readonly ParticlesManager _particlesManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly IDreamInterfaceManager _dreamInterfaceManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
public RenderTargetPool RenderTargetPool = default!;
private readonly Random _random = new();
private readonly RendererMetaData _defaultRenderMetaData = new(); //used for icon GetTexture(), never needs anything but default settings

public override void Initialize() {
base.Initialize();
SubscribeLocalEvent<DreamParticlesComponent, AfterAutoHandleStateEvent>(OnDreamParticlesComponentChange);
SubscribeLocalEvent<DreamParticlesComponent, ComponentRemove>(HandleComponentRemove);
RenderTargetPool = new(_clyde);
}

private void OnDreamParticlesComponentChange(EntityUid uid, DreamParticlesComponent component, ref AfterAutoHandleStateEvent args)
{
if(_particlesManager.TryGetParticleSystem(uid, out var system))
system.UpdateSystem(GetParticleSystemArgs(component));
else
_particlesManager.CreateParticleSystem(uid, GetParticleSystemArgs(component));
}

private void HandleComponentRemove(EntityUid uid, DreamParticlesComponent component, ref ComponentRemove args) {
_particlesManager.DestroyParticleSystem(uid);
}

private ParticleSystemArgs GetParticleSystemArgs(DreamParticlesComponent component){
Func<Texture?> textureFunc;
if(component.TextureList.Length == 0)
textureFunc = () => Texture.White;
else{
List<DreamIcon> icons = new(component.TextureList.Length);
foreach(var appearance in component.TextureList){
DreamIcon icon = new (RenderTargetPool, _dreamInterfaceManager, _gameTiming, _clyde, _appearanceSystem);
icon.SetAppearance(appearance.MustGetId());
icons.Add(icon);
}

textureFunc = () => _random.Pick(icons).GetTexture(null!, null!, _defaultRenderMetaData, null, null); //oh god, so hacky
}

var result = new ParticleSystemArgs(textureFunc, new Vector2i(component.Width, component.Height), (uint)component.Count, component.Spawning) {
Lifespan = GetGeneratorFloat(component.LifespanLow, component.LifespanHigh, component.LifespanDist),
Fadein = GetGeneratorFloat(component.FadeInLow, component.FadeInHigh, component.FadeInDist),
Fadeout = GetGeneratorFloat(component.FadeOutLow, component.FadeOutHigh, component.FadeOutDist)
};
if (component.Gradient.Length > 0)
result.Color = (lifetime) => {
var colorIndex = (int)(lifetime * component.Gradient.Length);
colorIndex = Math.Clamp(colorIndex, 0, component.Gradient.Length - 1);
return component.Gradient[colorIndex];
};
else
result.Color = (_) => Color.White;
result.Acceleration = (_ , velocity) => GetGeneratorVector3(component.AccelerationLow, component.AccelerationHigh, component.AccelerationType, component.AccelerationDist)() + GetGeneratorVector3(component.DriftLow, component.DriftHigh, component.DriftType, component.DriftDist)() - velocity*GetGeneratorVector3(component.FrictionLow, component.FrictionHigh, component.FrictionType, component.FrictionDist)();
result.SpawnPosition = GetGeneratorVector3(component.SpawnPositionLow, component.SpawnPositionHigh, component.SpawnPositionType, component.SpawnPositionDist);
result.SpawnVelocity = GetGeneratorVector3(component.SpawnVelocityLow, component.SpawnVelocityHigh, component.SpawnVelocityType, component.SpawnVelocityDist);
result.Transform = (_) => {
var scale = GetGeneratorVector2(component.ScaleLow, component.ScaleHigh, component.ScaleType, component.ScaleDist)();
var rotation = GetGeneratorFloat(component.RotationLow, component.RotationHigh, component.RotationDist)();
var growth = GetGeneratorVector2(component.GrowthLow, component.GrowthHigh, component.GrowthType, component.GrowthDist)();
var spin = GetGeneratorFloat(component.SpinLow, component.SpinHigh, component.SpinDist)();
return Matrix3x2.CreateScale(scale.X + growth.X, scale.Y + growth.Y) *
Matrix3x2.CreateRotation(rotation + spin);
};
result.BaseTransform = Matrix3x2.Identity;

return result;
}

private Func<float> GetGeneratorFloat(float low, float high, GeneratorDistribution distribution){
switch (distribution) {
case GeneratorDistribution.Constant:
return () => high;
case GeneratorDistribution.Uniform:
return () => _random.NextFloat(low, high);
case GeneratorDistribution.Normal:
return () => (float) Math.Clamp(_random.NextGaussian((low+high)/2, (high-low)/6), low, high);
case GeneratorDistribution.Linear:
return () => MathF.Sqrt(_random.NextFloat(0, 1)) * (high - low) + low;
case GeneratorDistribution.Square:
return () => MathF.Cbrt(_random.NextFloat(0, 1)) * (high - low) + low;
default:
throw new NotImplementedException();
}
}

private Func<Vector2> GetGeneratorVector2(Vector2 low, Vector2 high, GeneratorOutputType type, GeneratorDistribution distribution){
switch (type) {
case GeneratorOutputType.Num:
return () => new Vector2(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)());
case GeneratorOutputType.Vector:
return () => Vector2.Lerp(low, high, GetGeneratorFloat(0,1,distribution)());
case GeneratorOutputType.Box:
return () => new Vector2(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)());
case GeneratorOutputType.Circle:
var theta = _random.NextFloat(0, 360);
//polar -> cartesian, radius between low and high, angle uniform sample
return () => new Vector2(MathF.Cos(theta) * GetGeneratorFloat(low.X, high.X, distribution)(), MathF.Sin(theta) * GetGeneratorFloat(low.Y, high.Y, distribution)());
case GeneratorOutputType.Square:
return () =>
{
var x = GetGeneratorFloat(-high.X, high.X, distribution)();
var y = GetGeneratorFloat(-high.Y, high.Y, distribution)();
if (MathF.Abs(x) < low.X)
y = _random.NextByte() > 128 ? GetGeneratorFloat(-high.Y, -low.Y, distribution)() : GetGeneratorFloat(low.Y, high.Y, distribution)();
return new(x, y);
};
default:
throw new NotImplementedException($"Unimplemented generator output type {type}");
}
}

private Func<Vector3> GetGeneratorVector3(Vector3 low, Vector3 high, GeneratorOutputType type, GeneratorDistribution distribution){
switch (type) {
case GeneratorOutputType.Num:
return () => new Vector3(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)(), GetGeneratorFloat(low.Z, high.Z, distribution)());
case GeneratorOutputType.Vector:
return () => Vector3.Lerp(low, high, GetGeneratorFloat(0,1,distribution)());
case GeneratorOutputType.Box:
return () => new Vector3(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)(), GetGeneratorFloat(low.Z, high.Z, distribution)());
case GeneratorOutputType.Sphere:
var theta = _random.NextFloat(0, 360);
var phi = _random.NextFloat(0, 180);
//3d polar -> cartesian, radius between low and high, angle uniform sample
return () => new Vector3(
MathF.Cos(theta) * MathF.Sin(phi) * GetGeneratorFloat(low.X, high.X, distribution)(),
MathF.Sin(theta) * MathF.Sin(phi) * GetGeneratorFloat(low.Y, high.Y, distribution)(),
MathF.Cos(phi) * GetGeneratorFloat(low.Z, high.Z, distribution)()
);
case GeneratorOutputType.Cube:
return () =>
{
var x = GetGeneratorFloat(-high.X, high.X, distribution)();
var y = GetGeneratorFloat(-high.Y, high.Y, distribution)();
var z = GetGeneratorFloat(-high.Z, high.Z, distribution)();
if (MathF.Abs(x) < low.X)
y = _random.NextByte() > 128 ? GetGeneratorFloat(-high.Y, -low.Y, distribution)() : GetGeneratorFloat(low.Y, high.Y, distribution)();
if (MathF.Abs(y) < low.Y)
z = _random.NextByte() > 128 ? GetGeneratorFloat(-high.Z, -low.Z, distribution)() : GetGeneratorFloat(low.Z, high.Z, distribution)();
return new(x, y, z);
};
case GeneratorOutputType.Circle:
case GeneratorOutputType.Square:
return () => new Vector3(GetGeneratorVector2(new(low.X, low.Y), new(high.X, high.Y), type, distribution)(),0);
default:
throw new NotImplementedException($"Unimplemented generator output type {type}");
}
}
}


Loading
Loading