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
1 #!/usr/bin/python3
2 # Christmas 2023 event generator (overwrites world/map/npc/annuals/xmas/2021.txt)
3
4 import sys, os, random, nanoid, xml, csv, time, traceback
5 from xml.dom import minidom
6 from xml.etree import ElementTree
7 from io import StringIO
8
9 # Argument: Path to tmwa server-data folder
10 GAME_PATH = sys.argv[1]; MAX_PRIZES=22
11
12 FILE = GAME_PATH+"/world/map/npc/annuals/xmas/2021.txt"
13 CLI = GAME_PATH+"/client-data/maps/"
14
15 # Retrieve list of maps
16 MAPS = os.listdir(CLI)
17 for m in list(MAPS):
18 MAPS.remove(m)
19 if m.endswith('.tmx'):
20 m=m.replace('.tmx','')
21 MAPS.append(m)
22 MAPS.remove('017-9'); MAPS.remove('botcheck') # Event maps are OK, but not those
23
24 # Some code stolen from testxml.py
25 def readAttrI(node, attr, dv):
26 return int(readAttr(node, attr, dv))
27
28 def readAttr(node, attr, dv):
29 try:
30 return node.attributes[attr].value
31 except:
32 traceback.print_exc()
33 return dv
34
35 ## Header with 2021 functions
36 HEADER="""// Christmas 2021-2023 Conversion Scripts
37 // This file was generated automatically.
38 // (C) The Mana World Team & Moubootaur Legends, 2021
39 // (C) The Mana World Team & Moubootaur Legends, 2023
40
41 function|script|ConvertChristmas21
42 {
43 return;
44 }
45 """
46
47 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"]
48 print("Generating Christmas 2023 event scripts... %d/%d rewards" % (MAX_PRIZES, len(rewards)))
49 with open(FILE, 'w') as f:
50 f.write(HEADER)
51
52 i=0
53 while i < MAX_PRIZES:
54 i+=1
55 myid = nanoid.generate("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", size=6)
56 myid = "%s%d" % (myid, i) # Obfuscation + Sequential
57
58 # Select the map
59 m = random.choice(MAPS)
60 MAPS.remove(m)
61 dom = minidom.parse("%s%s.tmx" % (CLI, m))
62 root = dom.documentElement
63 mapWidth = readAttrI(root, "width", 0)
64 mapHeight = readAttrI(root, "height", 0)
65 # Find the collision map so we can find random coordinates
66 layers = dom.getElementsByTagName("layer")
67 for layer in layers:
68 if readAttr(layer, "name", None).lower() == "collision":
69 collision = layer
70 break
71 # Dark magic below
72 collision = collision.getElementsByTagName("data")
73 for data in collision:
74 binData = data.childNodes[0].data.strip()
75 fo = StringIO(binData)
76 arr = list(csv.reader(fo, delimiter=',', quotechar='|'))
77 # arr contains the collision data, where only '0' is valid for us
78 # So now we do some brute-forcing within map margins
79 while True:
80 x = random.randint(20, mapWidth-20)
81 y = random.randint(20, mapHeight-20)
82 if int(arr[y][x]):
83 continue
84 break
85 ## And now, both m, x and y are set!
86
87 reward = random.choice(rewards) # What prize this disturbance gives
88 prize = random.randint(100000, 150000) # How much GP this disturbance requires
89 bf="""
90 %s,%d,%d,0|script|Disturbance#%s|176
91 {
92 if (gettime(6) < 11) goto L_Vanish;
93 if ($@MONEY_%s >= %d) goto L_Vanish;
94 set .@find, array_search(strcharinfo(0), $@XMAS23$);
95 if (.@find >= 0) goto L_NotYou;
96 goto L_Main;
97
98 L_Vanish:
99 message strcharinfo(0), "The disturbance mysteriously vanishes...";
100 disablenpc "Disturbance#%s";
101 end;
102
103 L_Broke:
104 mes l("You probably should sell your items first, so you have the specified amount of money in gold pieces.");
105 close;
106
107 L_NotYou:
108 mes l("I already closed a disturbance by myself, I should let others close the remaining ones.");
109 mes "##1"+l("WARNING: Closing them with alts still count as closing multiple disturbances and will affect negatively a future event.")+"##0";
110 close;
111
112 L_Excess:
113 mes l("That's too much money to throw on a whim. I shouldn't spend more than 200,000 GP at a time.");
114 close;
115
116 L_Close:
117 close;
118
119 L_Main:
120 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.");
121 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.");
122 mes "##9"+l("NOTE: Closing the disturbance will record your name in GM Logs. Closed disturbances will not re-open.")+"##0";
123 next;
124 mes l("Will you throw money at it?");
125 input @money;
126 if (@money < 1) goto L_Close;
127 if (Zeny < @money) goto L_Broke;
128 if (@money > 200000) goto L_Excess;
129 set Zeny, Zeny - @money;
130 getexp (@money * 3), (@money / 2);
131 set $MONEY_X23, $MONEY_X23 + @money;
132 set $@MONEY_%s, $@MONEY_%s + @money;
133 if ($@MONEY_%s >= %d) goto L_Reward;
134 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.");
135 close;
136
137 L_Reward:
138 getitem %s, 1;
139 set @i, getarraysize($@XMAS23$);
140 set $@XMAS23$[@i], strcharinfo(0);
141 set @i, 0;
142 gmlog "sealed a disturbance in map %s and found a %s behind.";
143 mes l("The disturbance shakes and vanishes - leaving a mysterious [@@"+%s+"|@@] in the place where the disturbance was...");
144 mes "##1"+l("WARNING: Closing them with alts still count as closing multiple disturbances and will affect negatively a future event.")+"##0";
145 disablenpc "Disturbance#%s";
146 close;
147 }
148 \n\n//===========================\n""" % (m, x, y, myid, myid, prize, myid, myid, myid, myid, prize, reward, m, reward, reward, myid)
149
150 rewards.remove(reward)
151 f.write(bf)
152
153 print("Generation complete.")
154
Rewards
22 of the 39 Rewards which Chronos sells for 1 Boss Medal were featured. However, the precise selection was generated randomly.