Event:Christmas 2023
| Christmas 2023 | |
|---|---|
| Start date | 12 December 2023 |
| End date | 31 December 2023 |
| Reoccuring | No |
“These manaworldians are greedy, they always have been, and this can be used for my advantage. just wait and see ...” ─ Zax De'Kagen
During Christmas 2023, Zax De'Kagen began interfering with the world (with visible effects as early as December 1st). When the event started, he left behind several disturbances which would need to be sealed off with money.
There was 22 disturbances in 22 different maps, including maps normally inaccessible. Each disturbance required a different amount of money to be sealed and gave a different prize. The event was pre-generated randomly and with obfuscation on server startup, making very difficult or even impossible for TMW Staff (including those with root or physical access) to know precisely the disturbance locations or their rewards.
The event would produce a "negative" score of 1 point per unclosed disturbance, and 3 points per disturbance closed by someone else's alt. A too high negative score could allow Zax De'Kagen to besiege the world again with a reduced experience rate (causing less experience to be earned overall until Zax is dealt with ─ if he's not dealt with, he might cause harm to the world, forcing several shops out of business or even destroying houses and sealing off areas). However, closing sufficient disturbances (without alts) could prevent the experience reduction, and even have the opposite effect on the experience rate.
The score was calculated manually based on GM Logs.
Generator Source Code
#!/usr/bin/python3
# Christmas 2023 event generator (overwrites world/map/npc/annuals/xmas/2021.txt)
import sys, os, random, nanoid, xml, csv, time, traceback
from xml.dom import minidom
from xml.etree import ElementTree
from io import StringIO
# Argument: Path to tmwa server-data folder
GAME_PATH = sys.argv[1]; MAX_PRIZES=22
FILE = GAME_PATH+"/world/map/npc/annuals/xmas/2021.txt"
CLI = GAME_PATH+"/client-data/maps/"
# Retrieve list of maps
MAPS = os.listdir(CLI)
for m in list(MAPS):
MAPS.remove(m)
if m.endswith('.tmx'):
m=m.replace('.tmx','')
MAPS.append(m)
MAPS.remove('017-9'); MAPS.remove('botcheck') # Event maps are OK, but not those
# Some code stolen from testxml.py
def readAttrI(node, attr, dv):
return int(readAttr(node, attr, dv))
def readAttr(node, attr, dv):
try:
return node.attributes[attr].value
except:
traceback.print_exc()
return dv
## Header with 2021 functions
HEADER="""// Christmas 2021-2023 Conversion Scripts
// This file was generated automatically.
// (C) The Mana World Team & Moubootaur Legends, 2021
// (C) The Mana World Team & Moubootaur Legends, 2023
function|script|ConvertChristmas21
{
return;
}
"""
rewards = ["MovieCap", "BlueWolfHelmet", "CloverHat", "RabbitEars", "Goggles", "LeatherGoggles", "Crown", "Cap", "GuyFawkesMask", "WitchDoctorsMask", "ElfNightcap", "Sunglasses", "ChristmasTreeHat", "SantaBeardHat", "MoubooHead", "PaperBag", "BunchOfParsley", "SkullMask", "SnowGoggles", "HeartGlasses", "OperaMask", "JesterMask", "WitchHat", "GoblinMask", "ChefHat", "EskimoHat", "AFKCap", "SmileyCap", "RedShades", "GreenShades", "DarkBlueShades", "YellowShades", "LightBlueShades", "PinkShades", "BlackShades", "OrangeShades", "PurpleShades", "DarkGreenShades", "SnowLauncher"]
print("Generating Christmas 2023 event scripts... %d/%d rewards" % (MAX_PRIZES, len(rewards)))
with open(FILE, 'w') as f:
f.write(HEADER)
i=0
while i < MAX_PRIZES:
i+=1
myid = nanoid.generate("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", size=6)
myid = "%s%d" % (myid, i) # Obfuscation + Sequential
# Select the map
m = random.choice(MAPS)
MAPS.remove(m)
dom = minidom.parse("%s%s.tmx" % (CLI, m))
root = dom.documentElement
mapWidth = readAttrI(root, "width", 0)
mapHeight = readAttrI(root, "height", 0)
# Find the collision map so we can find random coordinates
layers = dom.getElementsByTagName("layer")
for layer in layers:
if readAttr(layer, "name", None).lower() == "collision":
collision = layer
break
# Dark magic below
collision = collision.getElementsByTagName("data")
for data in collision:
binData = data.childNodes[0].data.strip()
fo = StringIO(binData)
arr = list(csv.reader(fo, delimiter=',', quotechar='|'))
# arr contains the collision data, where only '0' is valid for us
# So now we do some brute-forcing within map margins
while True:
x = random.randint(20, mapWidth-20)
y = random.randint(20, mapHeight-20)
if int(arr[y][x]):
continue
break
## And now, both m, x and y are set!
reward = random.choice(rewards) # What prize this disturbance gives
prize = random.randint(100000, 150000) # How much GP this disturbance requires
bf="""
%s,%d,%d,0|script|Disturbance#%s|176
{
if (gettime(6) < 11) goto L_Vanish;
if ($@MONEY_%s >= %d) goto L_Vanish;
set .@find, array_search(strcharinfo(0), $@XMAS23$);
if (.@find >= 0) goto L_NotYou;
goto L_Main;
L_Vanish:
message strcharinfo(0), "The disturbance mysteriously vanishes...";
disablenpc "Disturbance#%s";
end;
L_Broke:
mes l("You probably should sell your items first, so you have the specified amount of money in gold pieces.");
close;
L_NotYou:
mes l("I already closed a disturbance by myself, I should let others close the remaining ones.");
mes "##1"+l("WARNING: Closing them with alts still count as closing multiple disturbances and will affect negatively a future event.")+"##0";
close;
L_Excess:
mes l("That's too much money to throw on a whim. I shouldn't spend more than 200,000 GP at a time.");
close;
L_Close:
close;
L_Main:
mes l("This is a disturbance caused by Zax De'Kagen manipulations in the fabric of reality, using the powers acquired from absorbing the Ether Spirit a couple years ago. It seems intentional.");
mes l("It might be possible to seal it off by throwing ##Bmoney##b on it. Keeping it open might put the whole world at risk - or not - but closing multiple ones could be worse.");
mes "##9"+l("NOTE: Closing the disturbance will record your name in GM Logs. Closed disturbances will not re-open.")+"##0";
next;
mes l("Will you throw money at it?");
input @money;
if (@money < 1) goto L_Close;
if (Zeny < @money) goto L_Broke;
if (@money > 200000) goto L_Excess;
set Zeny, Zeny - @money;
getexp (@money * 3), (@money / 2);
set $MONEY_X23, $MONEY_X23 + @money;
set $@MONEY_%s, $@MONEY_%s + @money;
if ($@MONEY_%s >= %d) goto L_Reward;
mes l("The amount wasn't sufficient to close the disturbance, but it is now smaller. More money will need to be poured before it fully closes.");
close;
L_Reward:
getitem %s, 1;
set @i, getarraysize($@XMAS23$);
set $@XMAS23$[@i], strcharinfo(0);
set @i, 0;
gmlog "sealed a disturbance in map %s and found a %s behind.";
mes l("The disturbance shakes and vanishes - leaving a mysterious [@@"+%s+"|@@] in the place where the disturbance was...");
mes "##1"+l("WARNING: Closing them with alts still count as closing multiple disturbances and will affect negatively a future event.")+"##0";
disablenpc "Disturbance#%s";
close;
}
\n\n//===========================\n""" % (m, x, y, myid, myid, prize, myid, myid, myid, myid, prize, reward, m, reward, reward, myid)
rewards.remove(reward)
f.write(bf)
print("Generation complete.")
Rewards
22 of the 39 Rewards which Chronos sells for 1 Boss Medal were featured. However, the precise selection was generated randomly.