summary refs log tree commit diff stats
path: root/tests/manyloc/keineschweine/lib/sg_assets.nim
blob: ccd1d9280076a0bf8335650b1b61aa89084181cd (plain) (blame)
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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
import
  re, json, strutils, tables, math, os, math_helpers, 
  sg_packets, md5, zlib_helpers

when defined(NoSFML):
  import server_utils
  type TVector2i = object
    x*, y*: int32
  proc vec2i(x, y: int32): TVector2i =
    result.x = x
    result.y = y
else:
  import sfml, sfml_audio, sfml_stuff
when not defined(NoChipmunk):
  import chipmunk

type
  TChecksumFile* = object
    unpackedSize*: int
    sum*: MD5Digest
    compressed*: string
  PZoneSettings* = ref TZoneSettings
  TZoneSettings* = object
    vehicles: seq[PVehicleRecord]
    items: seq[PItemRecord]
    objects: seq[PObjectRecord]
    bullets: seq[PBulletRecord]
    levelSettings: PLevelSettings
  PLevelSettings* = ref TLevelSettings
  TLevelSettings* = object
    size*: TVector2i
    starfield*: seq[PSpriteSheet]
  PVehicleRecord* = ref TVehicleRecord
  TVehicleRecord* = object
    id*: int16
    name*: string
    playable*: bool
    anim*: PAnimationRecord
    physics*: TPhysicsRecord
    handling*: THandlingRecord
  TItemKind* = enum
    Projectile, Utility, Ammo
  PObjectRecord* = ref TObjectRecord
  TObjectRecord* = object
    id*: int16
    name*: string
    anim*: PAnimationRecord
    physics*: TPhysicsRecord
  PItemRecord* = ref TItemRecord
  TItemRecord* = object
    id*: int16
    name*: string
    anim*: PAnimationRecord
    physics*: TPhysicsRecord ##apply when the item is dropped in the arena
    cooldown*: float
    energyCost*: float
    useSound*: PSoundRecord
    case kind*: TItemKind
    of Projectile: 
      bullet*: PBulletRecord
    else: 
      nil
  PBulletRecord* = ref TBulletRecord
  TBulletRecord* = object
    id*: int16
    name*: string
    anim*: PAnimationRecord
    physics*: TPhysicsRecord
    lifetime*, inheritVelocity*, baseVelocity*: float
    explosion*: TExplosionRecord
    trail*: TTrailRecord
  TTrailRecord* = object
    anim*: PAnimationRecord
    timer*: float ##how often it should be created
  TPhysicsRecord* = object
    mass*: float
    radius*: float
    moment*: float
  THandlingRecord = object
    thrust*, top_speed*: float
    reverse*, strafe*, rotation*: float
  TSoulRecord = object
    energy*: int
    health*: int
  TExplosionRecord* = object
    anim*: PAnimationRecord
    sound*: PSoundRecord 
  PAnimationRecord* = ref TAnimationRecord
  TAnimationRecord* = object
    spriteSheet*: PSpriteSheet
    angle*: float
    delay*: float  ##animation delay
  PSoundRecord* = ref TSoundRecord
  TSoundRecord* = object
    file*: string
    when defined(NoSFML):
      contents*: TChecksumFile
    else:
      soundBuf*: PSoundBuffer 
  PSpriteSheet* = ref TSpriteSheet
  TSpriteSheet* = object 
    file*: string
    framew*,frameh*: int
    rows*, cols*: int
    when defined(NoSFML):
      contents*: TChecksumFile
    when not defined(NoSFML):
      sprite*: PSprite
      tex*: PTexture
  TGameState* = enum
    Lobby, Transitioning, Field
const
  TAU* = PI * 2.0
  MomentMult* = 0.62 ## global moment of inertia multiplier
var 
  cfg: PZoneSettings
  SpriteSheets* = initTable[string, PSpriteSheet](64)
  SoundCache  * = initTable[string, PSoundRecord](64)
  nameToVehID*: TTable[string, int]
  nameToItemID*: TTable[string, int]
  nameToObjID*: TTable[string, int]
  nameToBulletID*: TTable[string, int]
  activeState = Lobby

proc newSprite(filename: string; errors: var seq[string]): PSpriteSheet
proc load*(ss: PSpriteSheet): bool {.discardable.}
proc newSound(filename: string; errors: var seq[string]): PSoundRecord
proc load*(s: PSoundRecord): bool {.discardable.}

proc validateSettings*(settings: PJsonNode; errors: var seq[string]): bool
proc loadSettings*(rawJson: string, errors: var seq[string]): bool
proc loadSettingsFromFile*(filename: string, errors: var seq[string]): bool

proc fetchVeh*(name: string): PVehicleRecord
proc fetchItm*(itm: string): PItemRecord
proc fetchObj*(name: string): PObjectRecord
proc fetchBullet(name: string): PBulletRecord

proc importLevel(data: PJsonNode; errors: var seq[string]): PLevelSettings
proc importVeh(data: PJsonNode; errors: var seq[string]): PVehicleRecord
proc importObject(data: PJsonNode; errors: var seq[string]): PObjectRecord
proc importItem(data: PJsonNode; errors: var seq[string]): PItemRecord
proc importPhys(data: PJsonNode): TPhysicsRecord
proc importAnim(data: PJsonNode; errors: var seq[string]): PAnimationRecord
proc importHandling(data: PJsonNode): THandlingRecord
proc importBullet(data: PJsonNode; errors: var seq[string]): PBulletRecord
proc importSoul(data: PJsonNode): TSoulRecord
proc importExplosion(data: PJsonNode; errors: var seq[string]): TExplosionRecord
proc importSound(data: PJsonNode; errors: var seq[string]; fieldName: string = nil): PSoundRecord

## this is the only pipe between lobby and main.nim
proc getActiveState*(): TGameState =
  result = activeState
proc transition*() = 
  assert activeState == Lobby, "Transition() called from a state other than lobby!"
  activeState = Transitioning
proc doneWithSaidTransition*() =
  assert activeState == Transitioning, "Finished() called from a state other than transitioning!"
  activeState = Field


proc checksumFile*(filename: string): TChecksumFile =
  let fullText = readFile(filename)
  result.unpackedSize = fullText.len
  result.sum = toMD5(fullText)
  result.compressed = compress(fullText)
proc checksumStr*(str: string): TChecksumFile =
  result.unpackedSize = str.len
  result.sum = toMD5(str)
  result.compressed = compress(str)


##at this point none of these should ever be freed
proc free*(obj: PZoneSettings) =
  echo "Free'd zone settings"
proc free*(obj: PSpriteSheet) =
  echo "Free'd ", obj.file
proc free*(obj: PSoundRecord) =
  echo "Free'd ", obj.file

proc loadAllAssets*() =
  var 
    loaded = 0
    failed = 0
  for name, ss in SpriteSheets.pairs():
    if load(ss):
      inc loaded
    else:
      inc failed
  echo loaded," sprites loaded. ", failed, " sprites failed."
  loaded = 0
  failed = 0
  for name, s in SoundCache.pairs():
    if load(s):
      inc loaded
    else:
      inc failed
  echo loaded, " sounds loaded. ", failed, " sounds failed."
proc getLevelSettings*(): PLevelSettings =
  result = cfg.levelSettings

iterator playableVehicles*(): PVehicleRecord =
  for v in cfg.vehicles.items():
    if v.playable:
      yield v

template allAssets*(body: stmt) {.dirty.}=
  block: 
    var assetType = FGraphics
    for file, asset in pairs(SpriteSheets):
      body
    assetType = FSound
    for file, asset in pairs(SoundCache):
      body

template cacheImpl(procName, cacheName, resultType: expr; body: stmt) {.dirty, immediate.} =
  proc procName*(filename: string; errors: var seq[string]): resulttype =
    if hasKey(cacheName, filename):
      return cacheName[filename]
    new(result, free)
    body
    cacheName[filename] = result

template checkFile(path: expr): stmt {.dirty, immediate.} =
  if not existsFile(path):
    errors.add("File missing: "& path)

cacheImpl newSprite, SpriteSheets, PSpriteSheet:
  result.file = filename
  if filename =~ re"\S+_(\d+)x(\d+)\.\S\S\S":
    result.framew = strutils.parseInt(matches[0])
    result.frameh = strutils.parseInt(matches[1])
    checkFile("data/gfx"/result.file)  
  else:
    errors.add "Bad file: "&filename&" must be in format name_WxH.png"
    return

cacheImpl newSound, SoundCache, PSoundRecord:
  result.file = filename
  checkFile("data/sfx"/result.file)

proc expandPath*(assetType: TAssetType; fileName: string): string =
  result = "data/"
  case assetType
  of FGraphics: result.add "gfx/"
  of FSound:    result.add "sfx/"
  else: nil
  result.add fileName
proc expandPath*(fc: ScFileChallenge): string {.inline.} =
  result = expandPath(fc.assetType, fc.file)

when defined(NoSFML):
  proc load*(ss: PSpriteSheet): bool =
    if not ss.contents.unpackedSize == 0: return
    ss.contents = checksumFile(expandPath(FGraphics, ss.file))
    result = true
  proc load*(s: PSoundRecord): bool =
    if not s.contents.unpackedSize == 0: return
    s.contents = checksumFile(expandPath(FSound, s.file))
    result = true
else:
  proc load*(ss: PSpriteSheet): bool =
    if not ss.sprite.isNil: 
      return
    var image = sfml.newImage("data/gfx/"/ss.file)
    if image == nil:
      echo "Image could not be loaded"
      return
    let size = image.getSize()
    ss.rows = int(size.y / ss.frameh) #y is h
    ss.cols = int(size.x / ss.framew) #x is w
    ss.tex = newTexture(image)
    image.destroy()
    ss.sprite = newSprite()
    ss.sprite.setTexture(ss.tex, true)
    ss.sprite.setTextureRect(intrect(0, 0, ss.framew.cint, ss.frameh.cint))
    ss.sprite.setOrigin(vec2f(ss.framew / 2, ss.frameh / 2))
    result = true
  proc load*(s: PSoundRecord): bool =
    s.soundBuf = newSoundBuffer("data/sfx"/s.file)
    if not s.soundBuf.isNil:
      result = true

template addError(e: expr): stmt {.immediate.} =
  errors.add(e)
  result = false
proc validateSettings*(settings: PJsonNode, errors: var seq[string]): bool =
  result = true
  if settings.kind != JObject:
    addError("Settings root must be an object")
    return
  if not settings.hasKey("vehicles"):
    addError("Vehicles section missing")
  if not settings.hasKey("objects"):
    errors.add("Objects section is missing")
    result = false
  if not settings.hasKey("level"):
    errors.add("Level settings section is missing")
    result = false
  else:
    let lvl = settings["level"]
    if lvl.kind != JObject or not lvl.hasKey("size"):
      errors.add("Invalid level settings")
      result = false
    elif not lvl.hasKey("size") or lvl["size"].kind != JArray or lvl["size"].len != 2:
      errors.add("Invalid/missing level size")
      result = false
  if not settings.hasKey("items"):
    errors.add("Items section missing")
    result = false
  else:
    let items = settings["items"]
    if items.kind != JArray or items.len == 0:
      errors.add "Invalid or empty item list"
    else:
      var id = 0
      for i in items.items:
        if i.kind != JArray: errors.add("Item #$1 is not an array"% $id)
        elif i.len != 3: errors.add("($1) Item record should have 3 fields"%($id))
        elif i[0].kind != JString or i[1].kind != JString or i[2].kind != JObject:
          errors.add("($1) Item should be in form [name, type, {item: data}]"% $id)
          result = false
        inc id

proc loadSettingsFromFile*(filename: string, errors: var seq[string]): bool =
  if not existsFile(filename):
    errors.add("File does not exist: "&filename)
  else:
    result = loadSettings(readFile(filename), errors)

proc loadSettings*(rawJson: string, errors: var seq[string]): bool =
  var settings: PJsonNode
  try:
    settings = parseJson(rawJson)
  except EJsonParsingError:
    errors.add("JSON parsing error: "& getCurrentExceptionMsg())
    return
  except: 
    errors.add("Unknown exception: "& getCurrentExceptionMsg())
    return
  if not validateSettings(settings, errors):
    return
  if cfg != nil: #TODO try this
    echo("Overwriting zone settings")
    free(cfg)
    cfg = nil
  new(cfg, free)
  cfg.levelSettings = importLevel(settings, errors)
  cfg.vehicles = @[]
  cfg.items = @[]
  cfg.objects = @[]
  cfg.bullets = @[]
  nameToVehID = initTable[string, int](32)
  nameToItemID = initTable[string, int](32)
  nameToObjID = initTable[string, int](32)
  nameToBulletID = initTable[string, int](32)
  var 
    vID = 0'i16
    bID = 0'i16
  for vehicle in settings["vehicles"].items:
    var veh = importVeh(vehicle, errors)
    veh.id = vID
    cfg.vehicles.add veh
    nameToVehID[veh.name] = veh.id
    inc vID
  vID = 0
  if settings.hasKey("bullets"):
    for blt in settings["bullets"].items:
      var bullet = importBullet(blt, errors)
      bullet.id = bID
      cfg.bullets.add bullet
      nameToBulletID[bullet.name] = bullet.id
      inc bID
  for item in settings["items"].items:
    var itm = importItem(item, errors)
    itm.id = vID
    cfg.items.add itm
    nameToItemID[itm.name] = itm.id
    inc vID
    if itm.kind == Projectile:
      if itm.bullet.isNil:
        errors.add("Projectile #$1 has no bullet!"% $vID)
      elif itm.bullet.id == -1:
        ## this item has an anonymous bullet, fix the ID and name
        itm.bullet.id = bID 
        itm.bullet.name = itm.name
        cfg.bullets.add itm.bullet
        nameToBulletID[itm.bullet.name] = itm.bullet.id
        inc bID
  vID = 0
  for obj in settings["objects"].items:
    var o = importObject(obj, errors)
    o.id = vID
    cfg.objects.add o
    nameToObjID[o.name] = o.id
    inc vID
  result = (errors.len == 0)

proc `$`*(obj: PSpriteSheet): string =
  return "<Sprite $1 ($2x$3) $4 rows $5 cols>" % [obj.file, $obj.framew, $obj.frameh, $obj.rows, $obj.cols]

proc fetchVeh*(name: string): PVehicleRecord =
  return cfg.vehicles[nameToVehID[name]]
proc fetchItm*(itm: string): PItemRecord =
  return cfg.items[nameToItemID[itm]]
proc fetchObj*(name: string): PObjectRecord =
  return cfg.objects[nameToObjID[name]]
proc fetchBullet(name: string): PBulletRecord =
  return cfg.bullets[nameToBulletID[name]]

proc getField(node: PJsonNode, field: string, target: var float) =
  if not node.hasKey(field):
    return
  if node[field].kind == JFloat:
    target = node[field].fnum
  elif node[field].kind == JInt:
    target = node[field].num.float
proc getField(node: PJsonNode, field: string, target: var int) =
  if not node.hasKey(field):
    return
  if node[field].kind == JInt:
    target = node[field].num.int
  elif node[field].kind == JFloat:
    target = node[field].fnum.int
proc getField(node: PJsonNode; field: string; target: var bool) =
  if not node.hasKey(field):
    return
  case node[field].kind
  of JBool:
    target = node[field].bval
  of JInt:
    target = (node[field].num != 0)
  of JFloat:
    target = (node[field].fnum != 0.0)
  else: nil

template checkKey(node: expr; key: string): stmt =
  if not hasKey(node, key):
    return

proc importTrail(data: PJsonNode; errors: var seq[string]): TTrailRecord =
  checkKey(data, "trail")
  result.anim = importAnim(data["trail"], errors)
  result.timer = 1000.0
  getField(data["trail"], "timer", result.timer)
  result.timer /= 1000.0
proc importLevel(data: PJsonNode; errors: var seq[string]): PLevelSettings =
  new(result)
  result.size = vec2i(5000, 5000)
  result.starfield = @[]
  
  checkKey(data, "level")
  var level = data["level"]
  if level.hasKey("size") and level["size"].kind == JArray and level["size"].len == 2:
    result.size.x = level["size"][0].num.cint
    result.size.y = level["size"][1].num.cint
  if level.hasKey("starfield"):
    for star in level["starfield"].items:
      result.starfield.add(newSprite(star.str, errors))
proc importPhys(data: PJsonNode): TPhysicsRecord =
  result.radius = 20.0
  result.mass = 10.0
  
  if data.hasKey("physics") and data["physics"].kind == JObject:
    let phys = data["physics"]
    phys.getField("radius", result.radius)
    phys.getField("mass", result.mass)
  when not defined(NoChipmunk):
    result.moment = momentForCircle(result.mass, 0.0, result.radius, vectorZero) * MomentMult
proc importHandling(data: PJsonNode): THandlingRecord =
  result.thrust = 45.0
  result.topSpeed = 100.0 #unused
  result.reverse = 30.0
  result.strafe = 30.0
  result.rotation = 2200.0
  
  checkKey(data, "handling")
  if data["handling"].kind != JObject:
    return
  
  let hand = data["handling"]
  hand.getField("thrust", result.thrust)
  hand.getField("top_speed", result.topSpeed)
  hand.getField("reverse", result.reverse)
  hand.getField("strafe", result.strafe)
  hand.getField("rotation", result.rotation)
proc importAnim(data: PJsonNode, errors: var seq[string]): PAnimationRecord =
  new(result)
  result.angle = 0.0
  result.delay = 1000.0
  result.spriteSheet = nil
  
  if data.hasKey("anim"):
    let anim = data["anim"]
    if anim.kind == JObject:
      if anim.hasKey("file"):
        result.spriteSheet = newSprite(anim["file"].str, errors)
      
      anim.getField "angle", result.angle
      anim.getField "delay", result.delay
    elif data["anim"].kind == JString:
      result.spriteSheet = newSprite(anim.str, errors)
  
  result.angle = radians(result.angle) ## comes in as degrees 
  result.delay /= 1000 ## delay comes in as milliseconds
proc importSoul(data: PJsonNode): TSoulRecord =
  result.energy = 10000
  result.health = 1
  checkKey(data, "soul")
  let soul = data["soul"]
  soul.getField("energy", result.energy)
  soul.getField("health", result.health)
proc importExplosion(data: PJsonNode; errors: var seq[string]): TExplosionRecord =
  checkKey(data, "explode")
  let expl = data["explode"]
  result.anim = importAnim(expl, errors)
  result.sound = importSound(expl, errors, "sound")
proc importSound*(data: PJsonNode; errors: var seq[string]; fieldName: string = nil): PSoundRecord =
  if data.kind == JObject:
    checkKey(data, fieldName)
    result = newSound(data[fieldName].str, errors)
  elif data.kind == JString:
    result = newSound(data.str, errors)

proc importVeh(data: PJsonNode; errors: var seq[string]): PVehicleRecord =
  new(result)
  result.playable = false
  if data.kind != JArray or data.len != 2 or 
    (data.kind == JArray and 
      (data[0].kind != JString or data[1].kind != JObject)):
    result.name = "(broken)"
    errors.add "Vehicle record is malformed"
    return
  var vehData = data[1]
  result.name = data[0].str
  result.anim = importAnim(vehdata, errors)
  result.physics = importPhys(vehdata)
  result.handling = importHandling(vehdata)
  vehdata.getField("playable", result.playable)
  if result.anim.spriteSheet.isNil and result.playable:
    result.playable = false
proc importObject(data: PJsonNode; errors: var seq[string]): PObjectRecord =
  new(result)
  if data.kind != JArray or data.len != 2:
    result.name = "(broken)"
    return
  result.name = data[0].str
  result.anim = importAnim(data[1], errors)
  result.physics = importPhys(data[1])
proc importItem(data: PJsonNode; errors: var seq[string]): PItemRecord =
  new(result)
  if data.kind != JArray or data.len != 3:
    result.name = "(broken)"
    errors.add "Item record is malformed"
    return
  result.name = data[0].str
  result.anim = importAnim(data[2], errors)
  result.physics = importPhys(data[2])
  
  result.cooldown = 100.0 
  data[2].getField("cooldown", result.cooldown)
  result.cooldown /= 1000.0  ##cooldown is stored in ms 
  
  result.useSound = importSound(data[2], errors, "useSound")
  
  case data[1].str.toLower
  of "projectile":
    result.kind = Projectile
    if data[2]["bullet"].kind == JString:
      result.bullet = fetchBullet(data[2]["bullet"].str)
    elif data[2]["bullet"].kind == JInt:
      result.bullet = cfg.bullets[data[2]["bullet"].num.int]
    elif data[2]["bullet"].kind == JObject: 
      result.bullet = importBullet(data[2]["bullet"], errors)
    else:
      errors.add "UNKNOWN BULLET TYPE for item "& result.name
  of "ammo":
    result.kind = Ammo
  of "utility":
    nil
  else:
    errors.add "Invalid item type \""& data[1].str &"\" for item "& result.name

proc importBullet(data: PJsonNode; errors: var seq[string]): PBulletRecord =
  new(result)
  result.id = -1
  
  var bdata: PJsonNode
  if data.kind == JArray:
    result.name = data[0].str
    bdata = data[1]
  elif data.kind == JObject:
    bdata = data
  else: 
    errors.add "Malformed bullet record"
    return
  
  result.anim = importAnim(bdata, errors)
  result.physics = importPhys(bdata)
  
  result.lifetime = 2000.0
  result.inheritVelocity = 1000.0
  result.baseVelocity = 30.0
  getField(bdata, "lifetime", result.lifetime)
  getField(bdata, "inheritVelocity", result.inheritVelocity)
  getField(bdata, "baseVelocity", result.baseVelocity)
  result.lifetime /= 1000.0 ## lifetime is stored as milliseconds
  result.inheritVelocity /= 1000.0 ## inherit velocity 1000 = 1.0 (100%)
  result.explosion = importExplosion(bdata, errors)
  result.trail = importTrail(bdata, errors)