123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- 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:
- discard
- 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
- MomentMult* = 0.62 ## global moment of inertia multiplier
- var
- cfg: PZoneSettings
- SpriteSheets* = initTable[string, PSpriteSheet](64)
- SoundCache * = initTable[string, PSoundRecord](64)
- nameToVehID*: Table[string, int]
- nameToItemID*: Table[string, int]
- nameToObjID*: Table[string, int]
- nameToBulletID*: Table[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: JsonNode; 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: JsonNode; errors: var seq[string]): PLevelSettings
- proc importVeh(data: JsonNode; errors: var seq[string]): PVehicleRecord
- proc importObject(data: JsonNode; errors: var seq[string]): PObjectRecord
- proc importItem(data: JsonNode; errors: var seq[string]): PItemRecord
- proc importPhys(data: JsonNode): TPhysicsRecord
- proc importAnim(data: JsonNode; errors: var seq[string]): PAnimationRecord
- proc importHandling(data: JsonNode): THandlingRecord
- proc importBullet(data: JsonNode; errors: var seq[string]): PBulletRecord
- proc importSoul(data: JsonNode): TSoulRecord
- proc importExplosion(data: JsonNode; errors: var seq[string]): TExplosionRecord
- proc importSound*(data: JsonNode; errors: var seq[string]; fieldName: string = ""): 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: untyped) {.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, body: untyped) {.dirty.} =
- 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: untyped) {.dirty.} =
- if not fileExists(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: discard
- 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: untyped) =
- errors.add(e)
- result = false
- proc validateSettings*(settings: JsonNode, 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 fileExists(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: JsonNode
- try:
- settings = parseJson(rawJson)
- except JsonParsingError:
- 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: JsonNode, 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: JsonNode, 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: JsonNode; 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: discard
- template checkKey(node: untyped; key: string) =
- if not hasKey(node, key):
- return
- proc importTrail(data: JsonNode; 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: JsonNode; 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: JsonNode): 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: JsonNode): 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: JsonNode, 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: JsonNode): 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: JsonNode; 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: JsonNode; errors: var seq[string]; fieldName: string = ""): 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: JsonNode; 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: JsonNode; 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: JsonNode; 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.toLowerAscii
- 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":
- discard
- else:
- errors.add "Invalid item type \""&data[1].str&"\" for item "&result.name
- proc importBullet(data: JsonNode; errors: var seq[string]): PBulletRecord =
- new(result)
- result.id = -1
- var bdata: JsonNode
- 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)
|