Graveyard
Table of Contents
{
"old_version": "1.2.1",
"depends": {
"StringUtils": "gy_",
"VmAbortReporter": "gy_"
}
}
1. About
Graveyard places gravestones where you die.
Gravestones are carried over play sessions, so:
- You die.
- You start a new game, or load the savegame.
- You see a gravestone where you died.
Features
- keys to remove gravestones on this map or all gravestones.
- doesn't affect gameplay.
- multiplayer compatible (players see other player's gravestones).
2. License
SPDX-FileCopyrightText: © 2020 Alexander Kromm (m8f) <mmaulwurff@gmail.com> SPDX-License-Identifier: GPL-3.0-only
3. Changelog
v1.2.1
- fixed double last death mark after loading a save
- spawn gravestones directly on the floor
- translatable new obituaries
v1.2.0
New:
- spawn gravestones immediately on death
- show last death on the automap
- add size variance to tombstones
Fixes:
- add tag to gravestone class
- use slightly less storage
- update VM abort handler
v1.1.1
- update VM abort handler
v1.1.0
- make stones fall to floor faster when spawning
- update Readme
- add vm abort handler
- rename patches directory to add-ons so GZDoom doesn't confuse it with textures
- spawn gravestones immediately in multiplayer
v1.0
- add real time to obituaries, fix code style
- add soulstones patch
- allow replacing stones with custom ones
v0.3.2
- fix crash when gravestones are on some locations on the map
- fix gravestones not loading even more
v0.3.1
- fix gravestones not loading sometimes more
v0.3
- fix gravestones not loading sometimes
- add randomized gravestone graphics
- add timestamp to grave description
v0.2
- fix new stones overwriting old ones after clearing stones on a map
v0.1: initial release
4. Keys
Alias "gy_remove_all" "netevent gy_remove_all" Alias "gy_remove_map" "netevent gy_remove_map" AddKeySection "$GY_TITLE" "gy_keys" AddMenuKey "$GY_REMOVE_ALL" "gy_remove_all" AddMenuKey "$GY_REMOVE_MAP" "gy_remove_map"
[enu default] GY_TITLE = "Graveyard";
5. Project setup
Modules used:
GameInfo
{
EventHandlers = "gy_EventHandler", "gy_VmAbortHandler"
}
version 4.14.3 #include "zscript/gy_death.zs" #include "zscript/gy_event_handler.zs" #include "zscript/gy_stone.zs" #include "zscript/gy_storage.zs" #include "zscript/gy_StringUtils.zs" #include "zscript/gy_VmAbortReporter.zs"
user bool gy_vm_abort_report_enabled = true;
6. Code
6.1. Event handler
class gy_EventHandler : EventHandler { override void worldTick() { if (_isFired) { return; } _isFired = true; removeStonesOnMap(); let storage = gy_Storage.of(); gy_Death death; while (death = storage.next()) { sendNetworkEvent("gy_spawn" .. death.toString()); } gy_Death lastDeath = storage.getLastDeathOnThisMap(); if (lastDeath != NULL) sendNetworkEvent("gy_mark" .. lastDeath.toString()); } // TODO: if rewriting Graveyard, separate translatable from verbatim text. // Translation should happen when an obituary is shown, not when it's created. override void worldThingDied(WorldEvent event) { Actor thing = event.thing; if (thing == NULL || thing.player == NULL) { return; } string killerPart = (event.thing.target == NULL) ? "" : string.format("\n%s: %s", StringTable.localize("$GY_KILLER"), thing.target.player ? thing.target.player.getUserName() : thing.target.getTag()); string obituary = string.format("%s: %s%s\n%s\n%s", StringTable.localize("$GY_VICTIM"), thing.player.getUserName(), killerPart, SystemTime.format("%F %T", _now), level.timeFormatted()); let storage = gy_Storage.of(); let death = gy_Death.of(thing.pos, obituary); storage.registerDeath(death); removeMarkers(); sendNetworkEvent("gy_mark" .. death.toString()); sendNetworkEvent("gy_spawn" .. death.toString()); } override void networkProcess(ConsoleEvent event) { if (event.name.left(8) == "gy_spawn") { let death = gy_Death.fromString(event.name.mid(8)); let pos = death.getLocation(); int seed = abs(int(pos.x + pos.y + pos.z)); int i = seed % 4; let c = string.format("gy_Stone%d", i); let stone = gy_Stone(Actor.spawn(c, death.getLocation(), ALLOW_REPLACE)); stone.setObituary(death.getObituary()); double scaleX = (1 + (seed % 13 - 6) / 6.0 / 10.0); double scaleY = (1 + (seed % 7 - 3) / 3.0 / 10.0); stone.a_setScale(scaleX, scaleY); } if (event.name.left(7) == "gy_mark") { removeMarkers(); let death = gy_Death.fromString(event.name.mid(7)); let pos = death.getLocation(); int seed = abs(int(pos.x + pos.y + pos.z)); int i = seed % 4; let c = string.format("gy_Marker%d", i); Actor.spawn(c, death.getLocation(), ALLOW_REPLACE); } else if (event.name == "gy_remove_all") { gy_Storage.of().clearAll(); removeStonesOnMap(); print("GY_REMOVE_ALL_MESSAGE"); } else if (event.name == "gy_remove_map") { gy_Storage.of().clearThisMap(); removeStonesOnMap(); print("GY_REMOVE_MAP_MESSAGE"); } } override void renderOverlay(RenderEvent event) { // Workaround to get the current time, which is UI-scoped. // Part 1/2. int second = level.time / TICRATE + 1; if (second > _lastSecond) { setNow(second, SystemTime.now()); } } private void removeMarkers() { foreach (marker : ThinkerIterator.create("gy_Marker", Thinker.STAT_MapMarker)) { marker.destroy(); break; } } private void print(string message) { Console.printf("%s", StringTable.localize(message, false)); } private void removeStonesOnMap() { let i = ThinkerIterator.create("gy_Stone"); Actor a; while (a = Actor(i.next())) { a.destroy(); } } // Workaround to get the current time, which is UI-scoped. // Part 2/2. private void setNow(int lastSecond, int now) const { _lastSecond = lastSecond; _now = now; } private transient bool _isFired; private int _lastSecond; private int _now; }
6.2. Death
class gy_Death { static gy_Death of(vector3 location, string obituary) { let result = new("gy_Death"); result._location = location; result._obituary = obituary; return result; } static gy_Death fromString(string s) { Array<string> t; s.split(t, ":"); vector3 location = (t[0].toDouble(), t[1].toDouble(), t[2].toDouble()); string obituary = t[3]; for (int i = 4; i < t.size(); ++i) { obituary.appendFormat(":%s", t[i]); } return gy_Death.of(location, obituary); } string toString() { return string.format("%.2f:%.2f:%.2f:%s", _location.x, _location.y, _location.z, _obituary); } vector3 getLocation() const { return _location; } string getObituary() const { return _obituary; } private vector3 _location; private string _obituary; }
6.3. Stone
class gy_Stone : Actor { Default { Radius 16; Height 1; Tag "$GY_GRAVESTONE"; } void setObituary(string obituary) { _obituary = obituary; } override bool used(Actor user) { Console.printf("%s", _obituary); return Super.used(user); } override void beginPlay() { setOrigin((pos.x, pos.y, floorZ), false); } private string _obituary; } class gy_Stone0 : gy_Stone { States { Spawn: gy_t a -1; Stop; } } class gy_Stone1 : gy_Stone { States { Spawn: gy_t b -1; Stop; } } class gy_Stone2 : gy_Stone { States { Spawn: gy_t c -1; Stop; } } class gy_Stone3 : gy_Stone { States { Spawn: gy_t d -1; Stop; } }
sprite gy_ta0, 33, 49 { patch gy_tomb1, 0, 0 offset 16, 49 xScale 1.5 yScale 1.8 } sprite gy_tb0, 33, 49 { patch gy_tomb2, 0, 0 offset 16, 49 xScale 1.5 yScale 1.8 } sprite gy_tc0, 32, 30 { patch gy_tomb3, 0, 0 offset 16, 30 xScale 1.1 yScale 1.32 } sprite gy_td0, 38, 34 { patch gy_tomb4, 0, 0 offset 19, 34 xScale 1.1 yScale 1.32 }
6.4. Automap marker
class gy_Marker : MapMarker {} class gy_Marker0 : gy_Marker { States { Spawn: gy_t a -1; Stop; } } class gy_Marker1 : gy_Marker { States { Spawn: gy_t b -1; Stop; } } class gy_Marker2 : gy_Marker { States { Spawn: gy_t c -1; Stop; } } class gy_Marker3 : gy_Marker { States { Spawn: gy_t d -1; Stop; } }
6.5. Storage
nosave string gy_hashes = ""; nosave string gy_locations = ""; nosave string gy_obituaries = "";
class gy_Storage { static gy_Storage of() { let result = new("gy_Storage"); result._hashes = Dictionary.fromString(Cvar.getCvar("gy_hashes" ).getString()); result._locations = Dictionary.fromString(Cvar.getCvar("gy_locations" ).getString()); result._obituaries = Dictionary.fromString(Cvar.getCvar("gy_obituaries").getString()); result._hashesIterator = DictionaryIterator.create(result._hashes); return result; } gy_Death next() { string checksum = level.getChecksum(); bool hasNext; while ((hasNext = _hashesIterator.next()) && _hashesIterator.value() != checksum); return hasNext ? getDeathAt(_hashesIterator.key()) : NULL; } gy_Death getLastDeathOnThisMap() { string checksum = level.getChecksum(); int maxIndex = -1; let i = DictionaryIterator.create(_hashes); while (i.next()) { if (i.value() == checksum) maxIndex = max(maxIndex, i.key().toInt()); } return (maxIndex == -1) ? NULL : getDeathAt(string.format("%d", maxIndex)); } void registerDeath(gy_Death death) { string i = string.format("%d", getNewIndex()); let l = death.getLocation(); _hashes .insert(i, level.getChecksum()); _locations .insert(i, string.format("%.2f:%.2f:%.2f", l.x, l.y, l.z)); _obituaries.insert(i, death.getObituary()); write(); } void clearAll() { _hashes = Dictionary.create(); _locations = Dictionary.create(); _obituaries = Dictionary.create(); write(); } void clearThisMap() { Dictionary newHashes = Dictionary.create(); Dictionary newLocations = Dictionary.create(); Dictionary newObituaries = Dictionary.create(); int index = 0; string checksum = level.getChecksum(); let i = DictionaryIterator.create(_hashes); while (i.next()) { if (i.value() != checksum) { string s = string.format("%d", index); newHashes .insert(s, i.value()); newLocations .insert(s, _locations .at(i.key())); newObituaries.insert(s, _obituaries.at(i.key())); ++index; } } _hashes = newHashes; _locations = newLocations; _obituaries = newObituaries; write(); } private gy_Death getDeathAt(string i) { Array<string> lc; _locations.at(i).split(lc, ":"); vector3 location = (lc[0].toDouble(), lc[1].toDouble(), lc[2].toDouble()); return gy_Death.of(location, _obituaries.at(i)); } private int getNewIndex() { // Clearing gravestones on a particular map creates gaps in indexes, // that's why new index is not just dictionary size. int maxIndex = 0; let i = DictionaryIterator.create(_hashes); while (i.next()) maxIndex = max(maxIndex, i.key().toInt()); return maxIndex + 1; } private void write() { Cvar.getCvar("gy_hashes" ).setString(_hashes .toString()); Cvar.getCvar("gy_locations" ).setString(_locations .toString()); Cvar.getCvar("gy_obituaries").setString(_obituaries.toString()); } private Dictionary _hashes; private DictionaryIterator _hashesIterator; private Dictionary _locations; private Dictionary _obituaries; }
7. Patches
7.1. Soulspheres
// SPDX-FileCopyrightText: © 2020 Alexander Kromm (m8f) <mmaulwurff@gmail.com> // SPDX-License-Identifier: GPL-3.0-only version 4.14.3 class gy_SoulStone0 : gy_Stone0 replaces gy_Stone0 { override void beginPlay() { Actor.spawn("SoulSphere", pos, ALLOW_REPLACE); Super.beginPlay(); } } class gy_SoulStone1 : gy_Stone1 replaces gy_Stone1 { override void beginPlay() { Actor.spawn("SoulSphere", pos, ALLOW_REPLACE); Super.beginPlay(); } } class gy_SoulStone2 : gy_Stone2 replaces gy_Stone2 { override void beginPlay() { Actor.spawn("SoulSphere", pos, ALLOW_REPLACE); Super.beginPlay(); } } class gy_SoulStone3 : gy_Stone3 replaces gy_Stone3 { override void beginPlay() { Actor.spawn("SoulSphere", pos, ALLOW_REPLACE); Super.beginPlay(); } }
7.2. Doom dead bodies
// SPDX-FileCopyrightText: © 2020 Alexander Kromm (m8f) <mmaulwurff@gmail.com> // SPDX-License-Identifier: GPL-3.0-only version 4.14.3 class gy_DoomDead0 : gy_Stone replaces gy_Stone0 { Default {+XFLIP} States { Spawn: PLAY N -1; Stop; } } class gy_DoomDead1 : gy_Stone replaces gy_Stone1 { Default {+XFLIP} States { Spawn: PLAY W -1; Stop; } } class gy_DoomDead2 : gy_Stone replaces gy_Stone2 { States { Spawn: PLAY N -1; Stop; } } class gy_DoomDead3 : gy_Stone replaces gy_Stone3 { States { Spawn: PLAY W -1; Stop; } } // Note: XFLIP doesn't work on automap. class gy_DoomDeadMarker0 : gy_Marker replaces gy_Marker0 { States { Spawn: PLAY N -1; Stop; } } class gy_DoomDeadMarker1 : gy_Marker replaces gy_Marker1 { States { Spawn: PLAY W -1; Stop; } } class gy_DoomDeadMarker2 : gy_Marker replaces gy_Marker2 { States { Spawn: PLAY N -1; Stop; } } class gy_DoomDeadMarker3 : gy_Marker replaces gy_Marker3 { States { Spawn: PLAY W -1; Stop; } }
8. Test
Basic check: the mod can be loaded without errors.
wait 2; map map01; wait 2; quit