-
Notifications
You must be signed in to change notification settings - Fork 1
/
viewer.py
405 lines (324 loc) · 13.4 KB
/
viewer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# Cartoon Cartoon Summer Resort Map Viewer
import sys, os, json, re
from math import ceil
try:
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame
except:
print("Pygame not found. Please install it with 'pip insall pygame'.")
sys.exit()
(screenWidth, screenHeight) = (416, 320)
bgcolor = (200, 200, 200)
def parseMapDataToJson(data):
"""
The map data is in an unusual json-like format, probably
optimized for LingoScript. It uses square brackets [] for both
dictionaries and 1-dimensional arrays. This function converts it
to valid JSON so it is easier to work with.
"""
# remove all newlines from file
data = data.replace("\n", "")
# replace []s with {}s
data = data.replace('[', '{')
data = data.replace(']', '}')
# iterate over the map data and to convert {}s back to []s in the specific case of list
s = ""
depth = 0
inList = False
inString = False
for i,char in enumerate(data):
if char == ',':
s = ""
continue
if char == '"':
inString = not inString
if inList and s == "#COND:" and not char in [' ', '{']:
inList = False
s += char
if not inString:
s = s.strip()
if inList:
if char == '{':
if depth == 0:
data = data[:i] + '[' + data[i + 1:]
depth += 1
if char == '}':
depth -= 1
if depth == 0:
data = data[:i] + ']' + data[i + 1:]
inList = False
else:
if s in ["#location", "#COND", "#message"]:
depth = 0
inList = True
# iterate over file again and remove all extra whitespace
json = ""
inString = False
for i,char in enumerate(data):
json += char
if char == '"':
inString = not inString
if not inString:
json = json.strip()
# use regex to add quotes around all necessary fields (all of which happen to be prefixed with #)
json = re.sub(r'([#]{1}\w+)', r'"\1"', json)
return json
# Reads the given map data and separate each tile into it's own string in an array
def separateTileStrings(data):
data = data[slice(1, -1)] # remove bounding {} around whole file
s = ""
depth = 0
tiles = []
for char in data:
if depth == 0 and char == ',':
continue
s += char
if char == '{':
depth += 1
elif char == '}':
depth -= 1
if depth == 0:
tiles.append(s)
s = ""
return tiles
# Reads each tile string in the given data and convert it to a dict using json.loads
def jsonLoadTileData(data):
tiles = []
for tile in data:
try:
tiles.append(json.loads(tile.__str__()))
except:
continue
return tiles
def convertWhiteToAlpha(surface):
"""
Replaces white pixels on the given surface with transparency.
"""
arr = pygame.PixelArray(surface)
arr.replace((255, 255, 255), (255, 255, 255, 0))
del arr
def drawTiles(screen, tileData, episode, renderInvis=False, renderSprites=True, renderTiles=True):
"""
Draws the given tile data on the given screen using sprites
from the given episode.
"""
# clear screen
bgcolor = (255, 255, 255) # white
screen.fill(bgcolor)
# draw each tile
for tile in tileData:
if not "#location" in tile:
continue
(x, y, tileWidth, tileHeight) = (tile["#location"][0], tile["#location"][1],
tile["#width"], tile["#height"])
(shiftX, shiftY) = (tile["#WSHIFT"], tile["#HSHIFT"])
# skip tile if it is invisible when the game starts
if (not renderInvis) and (tile["#data"]["#item"]["#visi"]["#visiObj"] != "" or tile["#data"]["#item"]["#visi"]["#visiAct"] != ""):
continue
# skip tile if it has an invis condition and rendering invis objects
if (renderInvis) and (tile["#data"]["#item"]["#visi"]["#inviObj"] != "" or tile["#data"]["#item"]["#visi"]["#inviAct"] != ""):
continue
spriteSurface = pygame.Surface((tileWidth, tileHeight), pygame.SRCALPHA)
sprite = None
try:
# load sprite image
spriteMember = tile["#member"]
if "Tile" in spriteMember:
tileName = spriteMember.split('.x')[0]
sprite = pygame.image.load('ccsr/{}/map.tiles/{}.png'.format(episode, tileName))
# there is probably better way to do this than checking both folders
block = 'ccsr/{}/map.visuals/{}.png'.format(episode, spriteMember)
char = 'ccsr/{}/character.visuals/{}.png'.format(episode, spriteMember)
if os.path.exists(block):
sprite = pygame.image.load(block)
elif os.path.exists(char):
sprite = pygame.image.load(char)
except:
pass
# if sprite is missing, draw red box and continue
if sprite == None:
rect = (0, 0, tileWidth, tileHeight)
color = (255, 32, 32, 255) # red
if renderSprites:
pygame.draw.rect(spriteSurface, color, rect)
screen.blit(spriteSurface, (16*x, 16*y))
continue
# draw sprite as tile
if "tile" in tile["#member"].lower():
# width / 32 needs to be ceil'd rather than rounded.
# Python rounds 2.5 down to 2, which would cause missing tiles.
for i in list(range(ceil(tileWidth/32))):
for j in list(range(ceil(tileHeight/32))):
if renderTiles:
spriteSurface.blit(sprite, (i*32, j*32))
convertWhiteToAlpha(spriteSurface)
screen.blit(spriteSurface, (x * 16, y * 16))
continue
# draw sprite as block
if renderSprites:
spriteSurface.blit(sprite, (0, 0))
convertWhiteToAlpha(spriteSurface)
"""
Macromedia Director has something called a Registration Point
This is the 0,0 location in each sprite's individual internal coordinate system.
In CCSR (possibly a default setting in Director),
the registration point is always set to the center of the sprite,
not the top left.
Therefore, we must normalize the coordinate system,
and put the anchor point at the top left instead of the sprite's center.
This means we have to simply subtract W/2 from X and H/2 from Y
"""
anchorXDist = tileWidth // 2
anchorYDist = tileHeight // 2
newX = (x * 16) + shiftX - anchorXDist
newY = (y * 16) + shiftY - anchorYDist
screen.blit(spriteSurface, (newX, newY))
def openMapFile(filename):
"""
Opens the map file, parses it to JSON and returns the tile data array.
"""
with open(filename, "r") as file:
parsedJson = parseMapDataToJson(file.read())
tileStrings = separateTileStrings(parsedJson)
tileData = jsonLoadTileData(tileStrings)
return tileData
def main():
print("\nCartoon Cartoon Summer Resort Map Viewer (alpha)\n")
argc = len(sys.argv)
argv = sys.argv
mode = "default"
episode = 1
showInvis = False
# Check if a file was passed as an argument
if (argc > 1):
print("Loading file: {}".format(sys.argv[1]))
mode = "file"
else:
print("Choose an episode. \n [1] Episode 1: Pool Problems\n [2] Episode 2: Tennis Menace\n [3] Episode 3: Vivian vs. The Volcano\n [4] Episode 4: Disco Dilemma")
c = "0"
while not int(c) in [1, 2, 3, 4]:
c = sys.stdin.read(1)
episode = int(c)
print("Loading Episode {}...".format(episode))
# Open map data file
mapFile = None
(col, row) = 1, 6
if mode == "default":
mapFile = "ccsr/{}/map.data/0{}0{}.txt".format(episode, col, row)
elif mode == "file":
mapFile = sys.argv[1]
tileData = openMapFile(mapFile)
# Init pygame
screen = pygame.display.set_mode((screenWidth, screenHeight))
pygame.display.set_caption("Cartoon Cartoon Summer Resort Map Viewer")
icon = pygame.Surface((32, 32), pygame.SRCALPHA)
icon.blit(pygame.image.load("ccsr/1/character.visuals/player.normal.right.1.png"), (0, 0))
convertWhiteToAlpha(icon)
pygame.display.set_icon(icon)
grid = pygame.Surface((screenWidth, screenHeight), pygame.SRCALPHA) # create grid surface with opacity
showGrid = False
showSprites = True
showTiles = True
pygame.mixer.init()
snapSound = pygame.mixer.Sound("ccsr/1/sound/grab.wav")
discoverSound = pygame.mixer.Sound("ccsr/1/sound/discover.wav")
bumpSound = pygame.mixer.Sound("ccsr/1/sound/bump.wav")
chimesSound = pygame.mixer.Sound("ccsr/1/sound/chimes.wav")
# Main render loop
running = True
doRedraw = True
clock = pygame.time.Clock()
while running:
clock.tick(60)
if doRedraw:
# clear screen and render the current tileData
screen.fill(bgcolor)
drawTiles(screen, tileData, episode, showInvis, showSprites, showTiles)
# draw grid over the screen if showGrid is true
if showGrid:
for i in list(range(round(screenWidth/32))): # draw gridlines to surface
pygame.draw.line(grid, (0, 0, 0, 64), (32*i, 0), (32*i, screenHeight))
for j in list(range(round(screenHeight/32))):
pygame.draw.line(grid, (0, 0, 0, 64), (0, 32*j), (screenWidth, 32*j))
screen.blit(grid, (0,0)) # draw grid surface to screen
doRedraw = False
# flip the display
pygame.display.flip()
# handle pygame events
for event in pygame.event.get():
# handle pygame window closed
if event.type == pygame.QUIT:
running = False
# handle keyboard input
if event.type == pygame.KEYDOWN:
doRedraw = True
(oldCol, oldRow, oldEpisode) = (col, row, episode)
# ARROW KEYS: move between maps
# left
if event.key == pygame.K_LEFT:
col -= 1
# right
elif event.key == pygame.K_RIGHT:
col += 1
# up
elif event.key == pygame.K_UP:
row -= 1
# down
elif event.key == pygame.K_DOWN:
row += 1
# 1/2/3/4: load episodes
if event.key == pygame.K_1:
episode = 1
if event.key == pygame.K_2:
episode = 2
if event.key == pygame.K_3:
episode = 3
if event.key == pygame.K_4:
episode = 4
# G: show/hide grid
elif event.key == pygame.K_g:
showGrid = not showGrid
# T: toggle tiles
elif event.key == pygame.K_t:
showTiles = not showTiles
# S: toggle sprites
elif event.key == pygame.K_s:
showSprites = not showSprites
# C: capture snapshot of current map
elif event.key == pygame.K_c:
# play snapshot sound
pygame.mixer.Sound.play(snapSound)
# make sure snaps folder exists
if not os.path.exists("./snaps"):
os.mkdir("snaps")
mapName = os.path.splitext(os.path.basename(mapFile))[0]
# save each snap of the same map with an increasing suffix
i = 1
while os.path.exists("./snaps/{}_{}.png".format(mapName, i)):
i+= 1
pygame.image.save(screen, "snaps/{}_{}.png".format(mapName, i))
# V: toggle showing invisible tiles
elif event.key == pygame.K_v:
# toggle show invis objects flag
showInvis = not showInvis
# play discover sound
if showInvis:
pygame.mixer.Sound.play(discoverSound)
# open new map data file if the col or row changed
if mode == "default" and (col, row, episode) != (oldCol, oldRow, oldEpisode):
newMapFile = "ccsr/{}/map.data/0{}0{}.txt".format(episode, col, row)
if os.path.exists(newMapFile):
# play chimes sound if episode changed
if episode != oldEpisode:
pygame.mixer.Sound.play(chimesSound)
# load new map file
mapFile = newMapFile
tileData = openMapFile(mapFile)
else:
# play bump sound and undo changes if error loading file
pygame.mixer.Sound.play(bumpSound)
(col, row, episode) = (oldCol, oldRow, oldEpisode)
print("We hope you enjoyed your stay!")
return
if __name__ == "__main__":
main()