diff --git a/sources/Commands/ACommandSpawn.uc b/sources/Commands/ACommandSpawn.uc
new file mode 100644
index 0000000..9acde3c
--- /dev/null
+++ b/sources/Commands/ACommandSpawn.uc
@@ -0,0 +1,130 @@
+/**
+ * Command for spawning new entities into the world.
+ * Copyright 2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class ACommandSpawn extends Command;
+
+// TODO: use spawned name for errors output?
+var private ACommandSpawn_Announcer announcer;
+
+protected function Finalizer()
+{
+ _.memory.Free(announcer);
+ super.Finalizer();
+}
+
+protected function BuildData(CommandDataBuilder builder)
+{
+ builder.Name(P("spawn")).Group(P("debug"))
+ .Summary(P("Spawns new entity on the map."));
+ builder.ParamText(P("template"))
+ .Describe(P("Spawns new entity based on the given template at the point"
+ @ "player is currently looking at."));
+ builder.SubCommand(P("at"))
+ .ParamText(P("template"))
+ .ParamNumber(P("x"))
+ .ParamNumber(P("y"))
+ .ParamNumber(P("z"))
+ .Describe(P("Spawns new entity based on the given template at"
+ @ "the point, given by the coordinates"));
+ announcer = ACommandSpawn_Announcer(
+ _.memory.Allocate(class'ACommandSpawn_Announcer'));
+}
+
+protected function Executed(
+ CallData arguments,
+ EPlayer instigator)
+{
+ local Text givenTemplate, template;
+ local Vector spawnLocation;
+
+ announcer.Setup(none, instigator, othersConsole);
+ givenTemplate = arguments.parameters.GetText(P("template"));
+ if (givenTemplate.StartsWithS("$")) {
+ template = _.alias.ResolveEntity(givenTemplate, true);
+ }
+ else {
+ template = givenTemplate.Copy();
+ }
+ _.memory.Free(givenTemplate);
+ if (arguments.subCommandName.IsEmpty()) {
+ SpawnInInstigatorSight(instigator, template);
+ }
+ else if (arguments.subCommandName.Compare(P("at"), SCASE_INSENSITIVE))
+ {
+ spawnLocation.x = arguments.parameters.GetFloat(P("x"));
+ spawnLocation.y = arguments.parameters.GetFloat(P("y"));
+ spawnLocation.z = arguments.parameters.GetFloat(P("z"));
+ SpawnAt(instigator, template, spawnLocation);
+ }
+ _.memory.Free(template);
+}
+
+private final function SpawnAt(
+ EPlayer instigator,
+ BaseText template,
+ Vector spawnLocation)
+{
+ local EPlaceable result;
+
+ result = _.kf.world.Spawn(template, spawnLocation);
+ if (result != none) {
+ announcer.AnnounceSpawned(template);
+ }
+ else {
+ announcer.AnnounceSpawningFailed(template);
+ }
+ _.memory.Free(result);
+}
+
+private final function SpawnInInstigatorSight(
+ EPlayer instigator,
+ BaseText template)
+{
+ local EPlaceable result;
+ local Vector spawnLocation;
+ local TracingIterator iter;
+
+ iter = _.kf.world.TracePlayerSight(instigator).LeaveOnlyVisible();
+ if (iter.HasFinished())
+ {
+ announcer.AnnounceFailedTrace();
+ return;
+ }
+ spawnLocation = iter.GetHitLocation();
+ result = _.kf.world.Spawn(template, spawnLocation);
+ // Shift position back a little and try again;
+ // this should fix a ton of spawning failures
+ if (result == none)
+ {
+ spawnLocation = spawnLocation +
+ Normal(iter.GetTracingStart() - spawnLocation) * 100;
+ result = _.kf.world.Spawn(template, spawnLocation);
+ }
+ if (result != none) {
+ announcer.AnnounceSpawned(template);
+ }
+ else {
+ announcer.AnnounceSpawningFailed(template);
+ }
+ _.memory.Free(result);
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Commands/ACommandSpawn_Announcer.uc b/sources/Commands/ACommandSpawn_Announcer.uc
new file mode 100644
index 0000000..7491a2c
--- /dev/null
+++ b/sources/Commands/ACommandSpawn_Announcer.uc
@@ -0,0 +1,90 @@
+/**
+ * Announcer for `ACommandSpawn`.
+ * Copyright 2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class ACommandSpawn_Announcer extends CommandAnnouncer;
+
+var private AnnouncementVariations spawned, spawningFailed, failedTrace;
+
+protected function Finalizer()
+{
+ FreeVariations(spawned);
+ FreeVariations(spawningFailed);
+ FreeVariations(failedTrace);
+ super.Finalizer();
+}
+
+public final function AnnounceSpawned(BaseText template)
+{
+ local int i;
+ local array templates;
+
+ if (!spawned.initialized)
+ {
+ spawned.initialized = true;
+ spawned.toSelfReport = _.text.MakeTemplate_S(
+ "You {$TextPositive spawned} {$TextEmphasis %1}!");
+ spawned.toSelfPublic = _.text.MakeTemplate_S(
+ "%%instigator%% {$TextNeutral spawned} {$TextEmphasis %1}!");
+ }
+ templates = MakeArray(spawned);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset().Arg(template);
+ }
+ MakeAnnouncement(spawned);
+}
+
+public final function AnnounceSpawningFailed(BaseText template)
+{
+ local int i;
+ local array templates;
+
+ if (!spawningFailed.initialized)
+ {
+ spawningFailed.initialized = true;
+ spawningFailed.toSelfReport = _.text.MakeTemplate_S(
+ "{$TextFailure Couldn't spawn} {$TextEmphasis %1}!");
+ }
+ templates = MakeArray(spawningFailed);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset().Arg(template);
+ }
+ MakeAnnouncement(spawningFailed);
+}
+
+public final function AnnounceFailedTrace()
+{
+ local int i;
+ local array templates;
+
+ if (!failedTrace.initialized)
+ {
+ failedTrace.initialized = true;
+ failedTrace.toSelfReport = _.text.MakeTemplate_S(
+ "{$TextFailure Failed} to trace spawn point");
+ }
+ templates = MakeArray(failedTrace);
+ for (i = 0; i < templates.length; i += 1) {
+ templates[i].Reset();
+ }
+ MakeAnnouncement(failedTrace);
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Futility_Feature.uc b/sources/Futility_Feature.uc
index 958f56f..e0a6712 100644
--- a/sources/Futility_Feature.uc
+++ b/sources/Futility_Feature.uc
@@ -1,7 +1,7 @@
/**
* This is the Futility feature, whose main purpose is to register commands
* from its package.
- * Copyright 2021 Anton Tarasenko
+ * Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Futility.
*
@@ -79,5 +79,6 @@ defaultproperties
allCommandClasses(4) = class'ACommandInventory'
allCommandClasses(5) = class'ACommandFeature'
allCommandClasses(6) = class'ACommandGod'
+ allCommandClasses(7) = class'ACommandSpawn'
errNoCommandsFeature = (l=LOG_Error,m="`Commands_Feature` is not detected, \"Futility\" will not be able to provide its functionality.")
}
\ No newline at end of file