diff --git a/Content.IntegrationTests/Tests/Nyanotrasen/Oracle/OracleTest.cs b/Content.IntegrationTests/Tests/Nyanotrasen/Oracle/OracleTest.cs
deleted file mode 100644
index c925db3ba21..00000000000
--- a/Content.IntegrationTests/Tests/Nyanotrasen/Oracle/OracleTest.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-#nullable enable
-using NUnit.Framework;
-using System.Threading.Tasks;
-using Content.Shared.Item;
-using Content.Shared.Mobs.Components;
-using Content.Server.Research.Oracle;
-using Content.Shared.Chemistry.Components;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-
-
-///
-/// The oracle's request pool is huge.
-/// We need to test everything that the oracle could request can be turned in.
-///
-namespace Content.IntegrationTests.Tests.Oracle
-{
- [TestFixture]
- [TestOf(typeof(OracleSystem))]
- public sealed class OracleTest
- {
- [Test]
- public async Task AllOracleItemsCanBeTurnedIn()
- {
- await using var pairTracker = await PoolManager.GetServerClient();
- var server = pairTracker.Server;
- // Per RobustIntegrationTest.cs, wait until state is settled to access it.
- await server.WaitIdleAsync();
-
- var mapManager = server.ResolveDependency();
- var prototypeManager = server.ResolveDependency();
- var entityManager = server.ResolveDependency();
- var entitySystemManager = server.ResolveDependency();
-
- var oracleSystem = entitySystemManager.GetEntitySystem();
- var oracleComponent = new OracleComponent();
-
- var testMap = await pairTracker.CreateTestMap();
-
- await server.WaitAssertion(() =>
- {
- var allProtos = oracleSystem.GetAllProtos(oracleComponent);
- var coordinates = testMap.GridCoords;
-
- Assert.That((allProtos.Count > 0), "Oracle has no valid prototypes!");
-
- foreach (var proto in allProtos)
- {
- var spawned = entityManager.SpawnEntity(proto, coordinates);
-
- Assert.That(entityManager.HasComponent(spawned),
- $"Oracle can request non-item {proto}");
-
- Assert.That(!entityManager.HasComponent(spawned),
- $"Oracle can request reagent container {proto} that will conflict with the fountain");
-
- Assert.That(!entityManager.HasComponent(spawned),
- $"Oracle can request mob {proto} that could potentially have a player-set name.");
- }
-
- // Because Server/Client pairs can be re-used between Tests, we
- // need to clean up anything that might affect other tests,
- // otherwise this pair cannot be considered clean, and the
- // CleanReturnAsync call would need to be removed.
- mapManager.DeleteMap(testMap.MapId);
- });
-
- await pairTracker.CleanReturnAsync();
- }
- }
-}
diff --git a/Content.Server/Nyanotrasen/Research/Oracle/OracleComponent.cs b/Content.Server/Nyanotrasen/Research/Oracle/OracleComponent.cs
deleted file mode 100644
index e238d5c7a18..00000000000
--- a/Content.Server/Nyanotrasen/Research/Oracle/OracleComponent.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using Content.Shared.Chemistry.Reagent;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Server.Research.Oracle;
-
-[RegisterComponent]
-public sealed partial class OracleComponent : Component
-{
- public const string SolutionName = "fountain";
-
- [ViewVariables]
- [DataField("accumulator")]
- public float Accumulator;
-
- [ViewVariables]
- [DataField("resetTime")]
- public TimeSpan ResetTime = TimeSpan.FromMinutes(10);
-
- [DataField("barkAccumulator")]
- public float BarkAccumulator;
-
- [DataField("barkTime")]
- public TimeSpan BarkTime = TimeSpan.FromMinutes(1);
-
- [DataField("rejectAccumulator")]
- public float RejectAccumulator;
-
- [DataField("rejectTime")]
- public TimeSpan RejectTime = TimeSpan.FromSeconds(5);
-
- [ViewVariables(VVAccess.ReadWrite)]
- public EntityPrototype DesiredPrototype = default!;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public EntityPrototype? LastDesiredPrototype = default!;
-
- [DataField("rewardReagents", customTypeSerializer: typeof(PrototypeIdListSerializer))]
- public IReadOnlyList RewardReagents = new[]
- {
- "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "Wine", "Blood", "Ichor"
- };
-
- [DataField("demandMessages")]
- public IReadOnlyList DemandMessages = new[]
- {
- "oracle-demand-1",
- "oracle-demand-2",
- "oracle-demand-3",
- "oracle-demand-4",
- "oracle-demand-5",
- "oracle-demand-6",
- "oracle-demand-7",
- "oracle-demand-8",
- "oracle-demand-9",
- "oracle-demand-10",
- "oracle-demand-11",
- "oracle-demand-12"
- };
-
- [DataField("rejectMessages")]
- public IReadOnlyList RejectMessages = new[]
- {
- "ἄγνοια",
- "υλικό",
- "ἀγνωσία",
- "γήινος",
- "σάκλας"
- };
-
- [DataField("blacklistedPrototypes")]
- [ViewVariables(VVAccess.ReadOnly)]
- public IReadOnlyList BlacklistedPrototypes = new[]
- {
- "Drone",
- "QSI",
- "HandTeleporter",
- "BluespaceBeaker",
- "ClothingBackpackHolding",
- "ClothingBackpackSatchelHolding",
- "ClothingBackpackDuffelHolding",
- "TrashBagOfHolding",
- "BluespaceCrystal",
- "InsulativeHeadcage",
- "CrystalNormality",
- };
-}
diff --git a/Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs b/Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs
deleted file mode 100644
index 148598fe2c3..00000000000
--- a/Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs
+++ /dev/null
@@ -1,258 +0,0 @@
-using System.Linq;
-using Content.Server.Botany;
-using Content.Server.Chat.Managers;
-using Content.Server.Chat.Systems;
-using Content.Server.Chemistry.Containers.EntitySystems;
-using Content.Server.Fluids.EntitySystems;
-using Content.Server.Psionics;
-using Content.Shared.Abilities.Psionics;
-using Content.Shared.Chat;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Interaction;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Psionics.Glimmer;
-using Content.Shared.Research.Prototypes;
-using Robust.Server.GameObjects;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-
-namespace Content.Server.Research.Oracle;
-
-public sealed class OracleSystem : EntitySystem
-{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ChatSystem _chat = default!;
- [Dependency] private readonly IChatManager _chatManager = default!;
- [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
- [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
- [Dependency] private readonly PuddleSystem _puddleSystem = default!;
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- foreach (var oracle in EntityQuery())
- {
- oracle.Accumulator += frameTime;
- oracle.BarkAccumulator += frameTime;
- oracle.RejectAccumulator += frameTime;
- if (oracle.BarkAccumulator >= oracle.BarkTime.TotalSeconds)
- {
- oracle.BarkAccumulator = 0;
- var message = Loc.GetString(_random.Pick(oracle.DemandMessages), ("item", oracle.DesiredPrototype.Name))
- .ToUpper();
- _chat.TrySendInGameICMessage(oracle.Owner, message, InGameICChatType.Speak, false);
- }
-
- if (oracle.Accumulator >= oracle.ResetTime.TotalSeconds)
- {
- oracle.LastDesiredPrototype = oracle.DesiredPrototype;
- NextItem(oracle);
- }
- }
- }
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnInteractHand);
- SubscribeLocalEvent(OnInteractUsing);
- }
-
- private void OnInit(EntityUid uid, OracleComponent component, ComponentInit args)
- {
- NextItem(component);
- }
-
- private void OnInteractHand(EntityUid uid, OracleComponent component, InteractHandEvent args)
- {
- if (!HasComp(args.User) || HasComp(args.User))
- return;
-
- if (!TryComp(args.User, out var actor))
- return;
-
- var message = Loc.GetString("oracle-current-item", ("item", component.DesiredPrototype.Name));
-
- var messageWrap = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message",
- ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")), ("message", message));
-
- _chatManager.ChatMessageToOne(ChatChannel.Telepathic,
- message, messageWrap, uid, false, actor.PlayerSession.ConnectedClient, Color.PaleVioletRed);
-
- if (component.LastDesiredPrototype != null)
- {
- var message2 = Loc.GetString("oracle-previous-item", ("item", component.LastDesiredPrototype.Name));
- var messageWrap2 = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message",
- ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")),
- ("message", message2));
-
- _chatManager.ChatMessageToOne(ChatChannel.Telepathic,
- message2, messageWrap2, uid, false, actor.PlayerSession.ConnectedClient, Color.PaleVioletRed);
- }
- }
-
- private void OnInteractUsing(EntityUid uid, OracleComponent component, InteractUsingEvent args)
- {
- if (HasComp(args.Used))
- return;
-
- if (!TryComp(args.Used, out var meta))
- return;
-
- if (meta.EntityPrototype == null)
- return;
-
- var validItem = CheckValidity(meta.EntityPrototype, component.DesiredPrototype);
-
- var nextItem = true;
-
- if (component.LastDesiredPrototype != null &&
- CheckValidity(meta.EntityPrototype, component.LastDesiredPrototype))
- {
- nextItem = false;
- validItem = true;
- component.LastDesiredPrototype = null;
- }
-
- if (!validItem)
- {
- if (!HasComp(args.Used) &&
- component.RejectAccumulator >= component.RejectTime.TotalSeconds)
- {
- component.RejectAccumulator = 0;
- _chat.TrySendInGameICMessage(uid, _random.Pick(component.RejectMessages), InGameICChatType.Speak, true);
- }
- return;
- }
-
- EntityManager.QueueDeleteEntity(args.Used);
-
- EntityManager.SpawnEntity("ResearchDisk5000", Transform(args.User).Coordinates);
-
- DispenseLiquidReward(uid, component);
-
- var i = _random.Next(1, 4);
-
- while (i != 0)
- {
- EntityManager.SpawnEntity("MaterialBluespace1", Transform(args.User).Coordinates);
- i--;
- }
-
- if (nextItem)
- NextItem(component);
- }
-
- private bool CheckValidity(EntityPrototype given, EntityPrototype target)
- {
- // 1: directly compare Names
- // name instead of ID because the oracle asks for them by name
- // this could potentially lead to like, labeller exploits maybe but so far only mob names can be fully player-set.
- if (given.Name == target.Name)
- return true;
-
- return false;
- }
-
- private void DispenseLiquidReward(EntityUid uid, OracleComponent component)
- {
- if (!_solutionSystem.TryGetSolution(uid, OracleComponent.SolutionName, out var fountainSol))
- return;
-
- var allReagents = _prototypeManager.EnumeratePrototypes()
- .Where(x => !x.Abstract)
- .Select(x => x.ID).ToList();
-
- var amount = 20 + _random.Next(1, 30) + _glimmerSystem.Glimmer / 10f;
- amount = (float) Math.Round(amount);
-
- var sol = new Solution();
- var reagent = "";
-
- if (_random.Prob(0.2f))
- reagent = _random.Pick(allReagents);
- else
- reagent = _random.Pick(component.RewardReagents);
-
- sol.AddReagent(reagent, amount);
-
- _solutionSystem.TryMixAndOverflow(fountainSol.Value, sol, fountainSol.Value.Comp.Solution.MaxVolume, out var overflowing);
-
- if (overflowing != null && overflowing.Volume > 0)
- _puddleSystem.TrySpillAt(uid, overflowing, out var _);
- }
-
- private void NextItem(OracleComponent component)
- {
- component.Accumulator = 0;
- component.BarkAccumulator = 0;
- component.RejectAccumulator = 0;
- var protoString = GetDesiredItem(component);
- if (_prototypeManager.TryIndex(protoString, out var proto))
- component.DesiredPrototype = proto;
- else
- Logger.Error("Oracle can't index prototype " + protoString);
- }
-
- private string GetDesiredItem(OracleComponent component)
- {
- return _random.Pick(GetAllProtos(component));
- }
-
-
- public List GetAllProtos(OracleComponent component)
- {
- var allTechs = _prototypeManager.EnumeratePrototypes();
- var allRecipes = new List();
-
- foreach (var tech in allTechs)
- {
- foreach (var recipe in tech.RecipeUnlocks)
- {
- var recipeProto = _prototypeManager.Index(recipe);
- allRecipes.Add(recipeProto.Result);
- }
- }
-
- var allPlants = _prototypeManager.EnumeratePrototypes().Select(x => x.ProductPrototypes[0])
- .ToList();
- var allProtos = allRecipes.Concat(allPlants).ToList();
- var blacklist = component.BlacklistedPrototypes.ToList();
-
- foreach (var proto in allProtos)
- {
- if (!_prototypeManager.TryIndex(proto, out var entityProto))
- {
- blacklist.Add(proto);
- continue;
- }
-
- if (!entityProto.Components.ContainsKey("Item"))
- {
- blacklist.Add(proto);
- continue;
- }
-
- if (entityProto.Components.ContainsKey("SolutionTransfer"))
- {
- blacklist.Add(proto);
- continue;
- }
-
- if (entityProto.Components.ContainsKey("MobState"))
- blacklist.Add(proto);
- }
-
- foreach (var proto in blacklist)
- {
- allProtos.Remove(proto);
- }
-
- return allProtos;
- }
-}
diff --git a/Content.Server/Research/Oracle/OracleComponent.cs b/Content.Server/Research/Oracle/OracleComponent.cs
new file mode 100644
index 00000000000..6196ce95060
--- /dev/null
+++ b/Content.Server/Research/Oracle/OracleComponent.cs
@@ -0,0 +1,73 @@
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Research.Oracle;
+
+[RegisterComponent]
+public sealed partial class OracleComponent : Component
+{
+ public const string SolutionName = "fountain";
+
+ [DataField(required: true)]
+ public ProtoId DemandTypes;
+
+ [DataField]
+ public List> BlacklistedDemands = new();
+
+ [DataField(required: true)]
+ public List> RewardEntities;
+
+ [DataField(required: true)]
+ public ProtoId RewardReagents;
+
+ ///
+ /// The chance to dispense a completely random chemical instead of what's listed in
+ ///
+ [DataField]
+ public float AbnormalReagentChance = 0.2f;
+
+ [DataField]
+ public TimeSpan
+ NextDemandTime = TimeSpan.Zero,
+ NextBarkTime = TimeSpan.Zero,
+ NextRejectTime = TimeSpan.Zero;
+
+ [DataField]
+ public TimeSpan
+ DemandDelay = TimeSpan.FromMinutes(10),
+ BarkDelay = TimeSpan.FromMinutes(2),
+ RejectDelay = TimeSpan.FromSeconds(10);
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public EntityPrototype DesiredPrototype = default!;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public EntityPrototype? LastDesiredPrototype = default!;
+
+ [DataField("demandMessages")]
+ public IReadOnlyList DemandMessages = new[]
+ {
+ "oracle-demand-1",
+ "oracle-demand-2",
+ "oracle-demand-3",
+ "oracle-demand-4",
+ "oracle-demand-5",
+ "oracle-demand-6",
+ "oracle-demand-7",
+ "oracle-demand-8",
+ "oracle-demand-9",
+ "oracle-demand-10",
+ "oracle-demand-11",
+ "oracle-demand-12"
+ };
+
+ [DataField("rejectMessages")]
+ public IReadOnlyList RejectMessages = new[]
+ {
+ "ἄγνοια",
+ "υλικό",
+ "ἀγνωσία",
+ "γήινος",
+ "σάκλας"
+ };
+}
diff --git a/Content.Server/Research/Oracle/OracleSystem.cs b/Content.Server/Research/Oracle/OracleSystem.cs
new file mode 100644
index 00000000000..63dcefbadd7
--- /dev/null
+++ b/Content.Server/Research/Oracle/OracleSystem.cs
@@ -0,0 +1,288 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.Botany;
+using Content.Server.Chat.Managers;
+using Content.Server.Chat.Systems;
+using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.Psionics;
+using Content.Server.Research.Systems;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Chat;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Interaction;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Psionics.Glimmer;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Research.Components;
+using Content.Shared.Research.Prototypes;
+using Content.Shared.Throwing;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Research.Oracle;
+
+public sealed class OracleSystem : EntitySystem
+{
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly IChatManager _chatMan = default!;
+ [Dependency] private readonly GlimmerSystem _glimmer = default!;
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
+ [Dependency] private readonly PuddleSystem _puddles = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ResearchSystem _research = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutions = default!;
+ [Dependency] private readonly ThrowingSystem _throwing = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Update(float frameTime)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (_timing.CurTime >= comp.NextDemandTime)
+ {
+ // Might be null if this is the first tick. In that case this will simply initialize it.
+ var last = (EntityPrototype?) comp.DesiredPrototype;
+ if (NextItem((uid, comp)))
+ comp.LastDesiredPrototype = last;
+ }
+
+ if (_timing.CurTime >= comp.NextBarkTime)
+ {
+ comp.NextBarkTime = _timing.CurTime + comp.BarkDelay;
+
+ var message = Loc.GetString(_random.Pick(comp.DemandMessages), ("item", comp.DesiredPrototype.Name)).ToUpper();
+ _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, false);
+ }
+ }
+
+ query.Dispose();
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInteractHand);
+ SubscribeLocalEvent(OnInteractUsing);
+ }
+
+ private void OnInteractHand(Entity oracle, ref InteractHandEvent args)
+ {
+ if (!HasComp(args.User) || HasComp(args.User)
+ || !TryComp(args.User, out var actor))
+ return;
+
+ SendTelepathicInfo(oracle, actor.PlayerSession.Channel,
+ Loc.GetString("oracle-current-item", ("item", oracle.Comp.DesiredPrototype.Name)));
+
+ if (oracle.Comp.LastDesiredPrototype != null)
+ SendTelepathicInfo(oracle, actor.PlayerSession.Channel,
+ Loc.GetString("oracle-previous-item", ("item", oracle.Comp.LastDesiredPrototype.Name)));
+ }
+
+ private void OnInteractUsing(Entity oracle, ref InteractUsingEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ if (HasComp(args.Used) || !TryComp(args.Used, out var meta) || meta.EntityPrototype == null)
+ return;
+
+ var requestValid = IsCorrectItem(meta.EntityPrototype, oracle.Comp.DesiredPrototype);
+ var updateRequest = true;
+
+ if (oracle.Comp.LastDesiredPrototype != null &&
+ IsCorrectItem(meta.EntityPrototype, oracle.Comp.LastDesiredPrototype))
+ {
+ updateRequest = false;
+ requestValid = true;
+ oracle.Comp.LastDesiredPrototype = null;
+ }
+
+ if (!requestValid)
+ {
+ if (!HasComp(args.Used) &&
+ _timing.CurTime >= oracle.Comp.NextRejectTime)
+ {
+ oracle.Comp.NextRejectTime = _timing.CurTime + oracle.Comp.RejectDelay;
+ _chat.TrySendInGameICMessage(oracle, _random.Pick(oracle.Comp.RejectMessages), InGameICChatType.Speak, true);
+ }
+
+ return;
+ }
+
+ DispenseRewards(oracle, Transform(args.User).Coordinates);
+ QueueDel(args.Used);
+
+ if (updateRequest)
+ NextItem(oracle);
+ }
+
+ private void SendTelepathicInfo(Entity oracle, INetChannel client, string message)
+ {
+ var messageWrap = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message",
+ ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")),
+ ("message", message));
+
+ _chatMan.ChatMessageToOne(ChatChannel.Telepathic,
+ message, messageWrap, oracle, false, client, Color.PaleVioletRed);
+ }
+
+ private bool IsCorrectItem(EntityPrototype given, EntityPrototype target)
+ {
+ // Nyano, what is this shit?
+ // Why are we comparing by name instead of prototype id?
+ // Why is this ever necessary?
+ // What were you trying to accomplish?!
+ if (given.Name == target.Name)
+ return true;
+
+ return false;
+ }
+
+ private void DispenseRewards(Entity oracle, EntityCoordinates throwTarget)
+ {
+ foreach (var rewardRandom in oracle.Comp.RewardEntities)
+ {
+ // Spawn each reward next to oracle and throw towards the target
+ var rewardProto = _protoMan.Index(rewardRandom).Pick(_random);
+ var reward = EntityManager.SpawnNextToOrDrop(rewardProto, oracle);
+ _throwing.TryThrow(reward, throwTarget, recoil: false);
+ }
+
+ DispenseLiquidReward(oracle);
+ }
+
+ private void DispenseLiquidReward(Entity oracle)
+ {
+ if (!_solutions.TryGetSolution(oracle.Owner, OracleComponent.SolutionName, out var fountainSol))
+ return;
+
+ // Why is this hardcoded?
+ var amount = MathF.Round(20 + _random.Next(1, 30) + _glimmer.Glimmer / 10f);
+ var temporarySol = new Solution();
+ var reagent = _protoMan.Index(oracle.Comp.RewardReagents).Pick(_random);
+
+ if (_random.Prob(oracle.Comp.AbnormalReagentChance))
+ {
+ var allReagents = _protoMan.EnumeratePrototypes()
+ .Where(x => !x.Abstract)
+ .Select(x => x.ID).ToList();
+
+ reagent = _random.Pick(allReagents);
+ }
+
+ temporarySol.AddReagent(reagent, amount);
+ _solutions.TryMixAndOverflow(fountainSol.Value, temporarySol, fountainSol.Value.Comp.Solution.MaxVolume, out var overflowing);
+
+ if (overflowing != null && overflowing.Volume > 0)
+ _puddles.TrySpillAt(oracle, overflowing, out var _);
+ }
+
+ private bool NextItem(Entity oracle)
+ {
+ oracle.Comp.NextBarkTime = oracle.Comp.NextRejectTime = TimeSpan.Zero;
+ oracle.Comp.NextDemandTime = _timing.CurTime + oracle.Comp.DemandDelay;
+
+ var protoId = GetDesiredItem(oracle);
+ if (protoId != null && _protoMan.TryIndex(protoId, out var proto))
+ {
+ oracle.Comp.DesiredPrototype = proto;
+ return true;
+ }
+
+ return false;
+ }
+
+ // TODO: find a way to not just use string literals here (weighted random doesn't support enums)
+ private string? GetDesiredItem(Entity oracle)
+ {
+ var demand = _protoMan.Index(oracle.Comp.DemandTypes).Pick(_random);
+
+ string? proto;
+ if (demand == "tech" && GetRandomTechProto(oracle, out proto))
+ return proto;
+
+ // This is also a fallback for when there's no research server to form an oracle tech request.
+ if (demand is "plant" or "tech" && GetRandomPlantProto(oracle, out proto))
+ return proto;
+
+ return null;
+ }
+
+ private bool GetRandomTechProto(Entity oracle, [NotNullWhen(true)] out string? proto)
+ {
+ // Try to find the most advanced server.
+ var database = _research.GetServerIds()
+ .Select(x => _research.TryGetServerById(x, out var serverUid, out _) ? serverUid : null)
+ .Where(x => x != null && Transform(x.Value).GridUid == Transform(oracle).GridUid)
+ .Select(x =>
+ {
+ TryComp(x!.Value, out var comp);
+ return new Entity(x.Value, comp);
+ })
+ .Where(x => x.Comp != null)
+ .OrderByDescending(x =>
+ _research.GetDisciplineTiers(x.Comp!).Select(pair => pair.Value).Max())
+ .FirstOrDefault(EntityUid.Invalid);
+
+ if (database.Owner == EntityUid.Invalid)
+ {
+ Log.Warning($"Cannot find an applicable server on grid {Transform(oracle).GridUid} to form an oracle request.");
+ proto = null;
+ return false;
+ }
+
+ // Select a technology that's either already unlocked, or can be unlocked from current research
+ var techs = _protoMan.EnumeratePrototypes()
+ .Where(x => !x.Hidden)
+ .Where(x =>
+ _research.IsTechnologyUnlocked(database.Owner, x, database.Comp)
+ || _research.IsTechnologyAvailable(database.Comp!, x))
+ .SelectMany(x => x.RecipeUnlocks)
+ .Select(x => _protoMan.Index(x).Result)
+ .Where(x => IsDemandValid(oracle, x))
+ .ToList();
+
+ // Unlikely.
+ if (techs.Count == 0)
+ {
+ proto = null;
+ return false;
+ }
+
+ proto = _random.Pick(techs);
+ return true;
+ }
+
+ private bool GetRandomPlantProto(Entity oracle, [NotNullWhen(true)] out string? proto)
+ {
+ var allPlants = _protoMan.EnumeratePrototypes()
+ .Select(x => x.ProductPrototypes.FirstOrDefault())
+ .Where(x => IsDemandValid(oracle, x))
+ .ToList();
+
+ if (allPlants.Count == 0)
+ {
+ proto = null;
+ return false;
+ }
+
+ proto = _random.Pick(allPlants)!;
+ return true;
+ }
+
+ private bool IsDemandValid(Entity oracle, ProtoId? id)
+ {
+ if (id == null || oracle.Comp.BlacklistedDemands.Contains(id.Value))
+ return false;
+
+ return _protoMan.TryIndex(id, out var proto) && proto.Components.ContainsKey("Item");
+ }
+}
diff --git a/Resources/Prototypes/Entities/Objects/Materials/bluespace.yml b/Resources/Prototypes/Entities/Objects/Materials/bluespace.yml
new file mode 100644
index 00000000000..f93534ecd5c
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Materials/bluespace.yml
@@ -0,0 +1,56 @@
+- type: entity
+ parent: MaterialBase
+ id: MaterialBluespace
+ suffix: Full
+ name: bluespace crystal
+ components:
+ - type: Sprite
+ sprite: Nyanotrasen/Objects/Materials/materials.rsi
+ layers:
+ - state: bluespace_3
+ map: ["base"]
+ - type: Appearance
+ - type: Material
+ - type: PhysicalComposition
+ materialComposition:
+ Bluespace: 100
+ - type: Tag
+ tags:
+ - BluespaceCrystal
+ - RawMaterial
+ - type: Stack
+ stackType: Bluespace
+ baseLayer: base
+ layerStates:
+ - bluespace
+ - bluespace_2
+ - bluespace_3
+ count: 5
+ - type: Item
+ size: Small
+
+- type: entity
+ parent: MaterialBluespace
+ id: MaterialBluespace1
+ suffix: 1
+ components:
+ - type: Sprite
+ state: bluespace
+ - type: Stack
+ count: 1
+
+- type: entity
+ parent: MaterialBluespace1
+ id: MaterialBluespace3
+ suffix: 3
+ components:
+ - type: Stack
+ count: 3
+
+- type: entity
+ parent: MaterialBluespace1
+ id: MaterialBluespace5
+ suffix: 5
+ components:
+ - type: Stack
+ count: 5
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml
index fa1b75530b6..862716c5123 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml
@@ -36,6 +36,15 @@
- type: ResearchDisk
points: 10000
+- type: entity
+ parent: ResearchDisk
+ id: ResearchDisk20000
+ name: research point disk (20000)
+ description: A disk for the R&D server containing 20000 points.
+ components:
+ - type: ResearchDisk
+ points: 20000
+
- type: entity
parent: ResearchDisk
id: ResearchDiskDebug
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/oracle.yml b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml
similarity index 55%
rename from Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/oracle.yml
rename to Resources/Prototypes/Entities/Structures/Specific/oracle.yml
index f7481abf1ed..51a25bffcdc 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/oracle.yml
+++ b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml
@@ -13,7 +13,6 @@
- state: oracle-0
- map: ["enum.SolutionContainerLayers.Fill"]
state: oracle-0
- - type: Oracle
- type: Speech
speechSounds: Tenor
- type: Psionic
@@ -42,3 +41,46 @@
- type: SpriteFade
- type: Tag
tags: []
+ - type: Oracle
+ demandTypes: OracleDemandTypes
+ rewardReagents: OracleRewardReagents
+ rewardEntities:
+ - OracleRewardDisks
+ - OracleRewardCrystals
+ demandBlacklist:
+ tags:
+ - Bluespace
+ components:
+ - MobState
+ demandWhitelist:
+ components:
+ - Item
+
+
+- type: weightedRandomEntity
+ id: OracleRewardDisks
+ weights:
+ ResearchDisk5000: 20
+ ResearchDisk10000: 5
+ ResearchDisk20000: 1
+
+- type: weightedRandomEntity
+ id: OracleRewardCrystals
+ weights:
+ MaterialBluespace1: 3
+ MaterialBluespace3: 10
+ MaterialBluespace5: 2
+
+- type: weightedRandom
+ id: OracleRewardReagents
+ weights:
+ LotophagoiOil: 7
+ Ichor: 2
+ Wine: 1.2
+ Blood: 0.8
+
+- type: weightedRandom
+ id: OracleDemandTypes
+ weights:
+ tech: 3
+ plant: 1 # Plants are very annoying to procure most of the time
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Materials/materials.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Materials/materials.yml
index 75bb4727da2..5aed17363ba 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Materials/materials.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Materials/materials.yml
@@ -1,47 +1,3 @@
-- type: entity
- parent: MaterialBase
- id: MaterialBluespace
- suffix: Full
- name: bluespace crystal
- components:
- - type: Sprite
- sprite: Nyanotrasen/Objects/Materials/materials.rsi
- layers:
- - state: bluespace_3
- map: ["base"]
- - type: Appearance
- - type: Material
- - type: PhysicalComposition
- materialComposition:
- Bluespace: 100
- - type: Tag
- tags:
- - BluespaceCrystal
- - RawMaterial
- - type: Stack
- stackType: Bluespace
- baseLayer: base
- layerStates:
- - bluespace
- - bluespace_2
- - bluespace_3
- count: 5
- - type: Item
- size: Small
-
-- type: entity
- parent: MaterialBluespace
- id: MaterialBluespace1
- suffix: 1
- components:
- - type: Sprite
- state: bluespace
- - type: Stack
- stackType: Bluespace
- count: 1
- - type: Item
- size: Tiny
-
- type: entity
parent: BaseItem
id: HideMothroach
diff --git a/Resources/Prototypes/Nyanotrasen/Reagents/psionic.yml b/Resources/Prototypes/Reagents/psionic.yml
similarity index 100%
rename from Resources/Prototypes/Nyanotrasen/Reagents/psionic.yml
rename to Resources/Prototypes/Reagents/psionic.yml