diff --git a/Art/Animations/.gitignore b/Art/Animations/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/Art/Animations/Destruction/DestructAfter.INI b/Art/Animations/Destruction/DestructAfter.INI new file mode 100644 index 00000000..6abcb362 --- /dev/null +++ b/Art/Animations/Destruction/DestructAfter.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=DestructAfter.flc +WALK= +RUN= +ATTACK1=DestructAfter.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Destruction/DestructAfter.flc b/Art/Animations/Destruction/DestructAfter.flc new file mode 100644 index 00000000..d2c82cee Binary files /dev/null and b/Art/Animations/Destruction/DestructAfter.flc differ diff --git a/Art/Animations/Destruction/DestructInitial.INI b/Art/Animations/Destruction/DestructInitial.INI new file mode 100644 index 00000000..a328548d --- /dev/null +++ b/Art/Animations/Destruction/DestructInitial.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=DestructInitial.flc +WALK= +RUN= +ATTACK1=DestructInitial.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Destruction/DestructInitial.flc b/Art/Animations/Destruction/DestructInitial.flc new file mode 100644 index 00000000..6abaa6c4 Binary files /dev/null and b/Art/Animations/Destruction/DestructInitial.flc differ diff --git a/Art/Animations/Districts/WindFarm/WindFarm.INI b/Art/Animations/Districts/WindFarm/WindFarm.INI new file mode 100644 index 00000000..49d99091 --- /dev/null +++ b/Art/Animations/Districts/WindFarm/WindFarm.INI @@ -0,0 +1,91 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=WindFarm.flc +WALK= +RUN= +ATTACK1=WindFarm.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= + +[Timing] +BLANK=0.500000 +DEFAULT=1.000000 +WALK=1.000000 +RUN=0.900000 +ATTACK1=1.000000 +ATTACK2=1.000000 +ATTACK3=1.000000 +DEFEND=1.000000 +DEATH=1.000000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +[Version] +VERSION=1 +[Palette] +PALETTE= \ No newline at end of file diff --git a/Art/Animations/Districts/WindFarm/WindFarm.flc b/Art/Animations/Districts/WindFarm/WindFarm.flc new file mode 100644 index 00000000..d85bd428 Binary files /dev/null and b/Art/Animations/Districts/WindFarm/WindFarm.flc differ diff --git a/Art/Animations/Districts/WindFarm/WindFarm_night.INI b/Art/Animations/Districts/WindFarm/WindFarm_night.INI new file mode 100644 index 00000000..f6763cc2 --- /dev/null +++ b/Art/Animations/Districts/WindFarm/WindFarm_night.INI @@ -0,0 +1,91 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=WindFarm_night.flc +WALK= +RUN= +ATTACK1=WindFarm_night.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= + +[Timing] +BLANK=0.500000 +DEFAULT=1.000000 +WALK=1.000000 +RUN=0.900000 +ATTACK1=1.000000 +ATTACK2=1.000000 +ATTACK3=1.000000 +DEFEND=1.000000 +DEATH=1.000000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +[Version] +VERSION=1 +[Palette] +PALETTE= \ No newline at end of file diff --git a/Art/Animations/Districts/WindFarm/WindFarm_night.flc b/Art/Animations/Districts/WindFarm/WindFarm_night.flc new file mode 100644 index 00000000..c083a0f5 Binary files /dev/null and b/Art/Animations/Districts/WindFarm/WindFarm_night.flc differ diff --git a/Art/Animations/NaturalWonders/AngelFalls.INI b/Art/Animations/NaturalWonders/AngelFalls.INI new file mode 100644 index 00000000..dbdcf34a --- /dev/null +++ b/Art/Animations/NaturalWonders/AngelFalls.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=170 +Fast Speed=170 + +[Animations] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1=AngelFalls.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/NaturalWonders/AngelFalls.flc b/Art/Animations/NaturalWonders/AngelFalls.flc new file mode 100644 index 00000000..69c8727b Binary files /dev/null and b/Art/Animations/NaturalWonders/AngelFalls.flc differ diff --git a/Art/Animations/NaturalWonders/AngelFalls_night.INI b/Art/Animations/NaturalWonders/AngelFalls_night.INI new file mode 100644 index 00000000..ab32fe25 --- /dev/null +++ b/Art/Animations/NaturalWonders/AngelFalls_night.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=170 +Fast Speed=170 + +[Animations] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1=AngelFalls_night.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/NaturalWonders/AngelFalls_night.flc b/Art/Animations/NaturalWonders/AngelFalls_night.flc new file mode 100644 index 00000000..6df8075f Binary files /dev/null and b/Art/Animations/NaturalWonders/AngelFalls_night.flc differ diff --git a/Art/Animations/Resources/Cow/Cow.pcx b/Art/Animations/Resources/Cow/Cow.pcx new file mode 100644 index 00000000..86baf26d Binary files /dev/null and b/Art/Animations/Resources/Cow/Cow.pcx differ diff --git a/Art/Animations/Resources/Cow/black and white cow 2.flc b/Art/Animations/Resources/Cow/black and white cow 2.flc new file mode 100644 index 00000000..86588b46 Binary files /dev/null and b/Art/Animations/Resources/Cow/black and white cow 2.flc differ diff --git a/Art/Animations/Resources/Cow/black and white cow.flc b/Art/Animations/Resources/Cow/black and white cow.flc new file mode 100644 index 00000000..1fe61ef1 Binary files /dev/null and b/Art/Animations/Resources/Cow/black and white cow.flc differ diff --git a/Art/Animations/Resources/Cow/black and white cow.txt b/Art/Animations/Resources/Cow/black and white cow.txt new file mode 100644 index 00000000..54c01fc2 --- /dev/null +++ b/Art/Animations/Resources/Cow/black and white cow.txt @@ -0,0 +1,21 @@ +Unit: Cow +Unit by: Tom2050 + +NO Civ Colors have been added. Made to be a flag unit; has default and fidget only. Walk may be added later. + +Enjoy! + +for C3X Districts + +black and white cow in motion +two sizes + +Reduced in size by Wotan49 + +greeting +Wotan49 +:old: + + + + diff --git a/Art/Animations/Resources/Cow/black_and_white_cow.INI b/Art/Animations/Resources/Cow/black_and_white_cow.INI new file mode 100644 index 00000000..8bad103d --- /dev/null +++ b/Art/Animations/Resources/Cow/black_and_white_cow.INI @@ -0,0 +1,91 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=black and white cow.flc +WALK= +RUN= +ATTACK1=black and white cow.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= + +[Timing] +BLANK=0.500000 +DEFAULT=1.000000 +WALK=1.000000 +RUN=0.900000 +ATTACK1=1.000000 +ATTACK2=1.000000 +ATTACK3=1.000000 +DEFEND=1.000000 +DEATH=1.000000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +[Version] +VERSION=1 +[Palette] +PALETTE= \ No newline at end of file diff --git a/Art/Animations/Resources/Cow/sora_cow.mp4 b/Art/Animations/Resources/Cow/sora_cow.mp4 new file mode 100644 index 00000000..799c1cf7 Binary files /dev/null and b/Art/Animations/Resources/Cow/sora_cow.mp4 differ diff --git a/Art/Animations/Resources/Deer/Deer.INI b/Art/Animations/Resources/Deer/Deer.INI new file mode 100644 index 00000000..3135f7cc --- /dev/null +++ b/Art/Animations/Resources/Deer/Deer.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=170 +Fast Speed=170 + +[Animations] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1=Deer.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Resources/Deer/Deer.flc b/Art/Animations/Resources/Deer/Deer.flc new file mode 100644 index 00000000..2af26cde Binary files /dev/null and b/Art/Animations/Resources/Deer/Deer.flc differ diff --git a/Art/Animations/Resources/Deer/Deer.pcx b/Art/Animations/Resources/Deer/Deer.pcx new file mode 100644 index 00000000..e0727872 Binary files /dev/null and b/Art/Animations/Resources/Deer/Deer.pcx differ diff --git a/Art/Animations/Resources/Deer/Deer_night.INI b/Art/Animations/Resources/Deer/Deer_night.INI new file mode 100644 index 00000000..ce9eea05 --- /dev/null +++ b/Art/Animations/Resources/Deer/Deer_night.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=170 +Fast Speed=170 + +[Animations] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1=Deer_night.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Resources/Deer/Deer_night.flc b/Art/Animations/Resources/Deer/Deer_night.flc new file mode 100644 index 00000000..6fd3eb36 Binary files /dev/null and b/Art/Animations/Resources/Deer/Deer_night.flc differ diff --git a/Art/Animations/Resources/Elephant/Elephant.flc b/Art/Animations/Resources/Elephant/Elephant.flc new file mode 100644 index 00000000..1c45eb05 Binary files /dev/null and b/Art/Animations/Resources/Elephant/Elephant.flc differ diff --git a/Art/Animations/Resources/Elephant/Elephant.ini b/Art/Animations/Resources/Elephant/Elephant.ini new file mode 100644 index 00000000..a40df7a0 --- /dev/null +++ b/Art/Animations/Resources/Elephant/Elephant.ini @@ -0,0 +1,80 @@ +[Speed] +Normal Speed=250 +Fast Speed=250 + +[Animations] +BLANK= +DEFAULT=Elephant.flc +WALK= +RUN= +ATTACK1=Elephant.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET=Elephant.flc +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +[Version] +VERSION=1 diff --git a/Art/Animations/Resources/Elephant/Elephant.pcx b/Art/Animations/Resources/Elephant/Elephant.pcx new file mode 100644 index 00000000..aa0a5837 Binary files /dev/null and b/Art/Animations/Resources/Elephant/Elephant.pcx differ diff --git a/Art/Animations/Resources/Elephant/ElephantLg.pcx b/Art/Animations/Resources/Elephant/ElephantLg.pcx new file mode 100644 index 00000000..ae60f7ad Binary files /dev/null and b/Art/Animations/Resources/Elephant/ElephantLg.pcx differ diff --git a/Art/Animations/Resources/Elephant/sora_elephant.mp4 b/Art/Animations/Resources/Elephant/sora_elephant.mp4 new file mode 100644 index 00000000..290e00ea Binary files /dev/null and b/Art/Animations/Resources/Elephant/sora_elephant.mp4 differ diff --git a/Art/Animations/Resources/Fish/20260301_091931.mp4 b/Art/Animations/Resources/Fish/20260301_091931.mp4 new file mode 100644 index 00000000..54dc5ac7 Binary files /dev/null and b/Art/Animations/Resources/Fish/20260301_091931.mp4 differ diff --git a/Art/Animations/Resources/Fish/Fish.INI b/Art/Animations/Resources/Fish/Fish.INI new file mode 100644 index 00000000..d5026d59 --- /dev/null +++ b/Art/Animations/Resources/Fish/Fish.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=Fish.flc +WALK= +RUN= +ATTACK1=Fish.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=10.500000 +DEFAULT=0.500000 +WALK=10.500000 +RUN=0.500000 +ATTACK1=10.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Resources/Fish/Fish.flc b/Art/Animations/Resources/Fish/Fish.flc new file mode 100644 index 00000000..383fc41a Binary files /dev/null and b/Art/Animations/Resources/Fish/Fish.flc differ diff --git a/Art/Animations/Resources/Fish/fish.pcx b/Art/Animations/Resources/Fish/fish.pcx new file mode 100644 index 00000000..83d163a9 Binary files /dev/null and b/Art/Animations/Resources/Fish/fish.pcx differ diff --git a/Art/Animations/Resources/Fish/fish_orig.pcx b/Art/Animations/Resources/Fish/fish_orig.pcx new file mode 100644 index 00000000..83d163a9 Binary files /dev/null and b/Art/Animations/Resources/Fish/fish_orig.pcx differ diff --git a/Art/Animations/Resources/Fish/sora_Fish.flc b/Art/Animations/Resources/Fish/sora_Fish.flc new file mode 100644 index 00000000..8aa93aa5 Binary files /dev/null and b/Art/Animations/Resources/Fish/sora_Fish.flc differ diff --git a/Art/Animations/Resources/Horse/Horse.pcx b/Art/Animations/Resources/Horse/Horse.pcx new file mode 100644 index 00000000..f3aaed6c Binary files /dev/null and b/Art/Animations/Resources/Horse/Horse.pcx differ diff --git a/Art/Animations/Resources/Horse/sora_horse.mp4 b/Art/Animations/Resources/Horse/sora_horse.mp4 new file mode 100644 index 00000000..3154f8c3 Binary files /dev/null and b/Art/Animations/Resources/Horse/sora_horse.mp4 differ diff --git a/Art/Animations/Resources/HorsePainted/HorsePainted.INI b/Art/Animations/Resources/HorsePainted/HorsePainted.INI new file mode 100644 index 00000000..8f5aa1ab --- /dev/null +++ b/Art/Animations/Resources/HorsePainted/HorsePainted.INI @@ -0,0 +1,81 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=HorsePainted.flc +WALK= +RUN= +ATTACK1=HorsePainted.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +[Version] +VERSION=1 +[Palette] +PALETTE= \ No newline at end of file diff --git a/Art/Animations/Resources/HorsePainted/HorsePainted.flc b/Art/Animations/Resources/HorsePainted/HorsePainted.flc new file mode 100644 index 00000000..d05bc4e2 Binary files /dev/null and b/Art/Animations/Resources/HorsePainted/HorsePainted.flc differ diff --git a/Art/Animations/Resources/HorsePainted/HorsePainted_ReadMe.txt b/Art/Animations/Resources/HorsePainted/HorsePainted_ReadMe.txt new file mode 100644 index 00000000..85014bd0 --- /dev/null +++ b/Art/Animations/Resources/HorsePainted/HorsePainted_ReadMe.txt @@ -0,0 +1,15 @@ +Unit: Horse (Painted) +Conversion by: Tom2050 +Unit has no Civ Color. +Unit is a conversion from Civ 4. + +Modifications made. + +Enjoy! + + +reduced from Wotan49 + +for C3X Districts + + diff --git a/Art/Animations/Resources/HorsePainted/HorsePainted_night.INI b/Art/Animations/Resources/HorsePainted/HorsePainted_night.INI new file mode 100644 index 00000000..7da7bc0d --- /dev/null +++ b/Art/Animations/Resources/HorsePainted/HorsePainted_night.INI @@ -0,0 +1,81 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=HorsePainted_night.flc +WALK= +RUN= +ATTACK1=HorsePainted_night.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +[Version] +VERSION=1 +[Palette] +PALETTE= \ No newline at end of file diff --git a/Art/Animations/Resources/HorsePainted/HorsePainted_night.flc b/Art/Animations/Resources/HorsePainted/HorsePainted_night.flc new file mode 100644 index 00000000..d9ba121b Binary files /dev/null and b/Art/Animations/Resources/HorsePainted/HorsePainted_night.flc differ diff --git a/Art/Animations/Resources/MilkCow/MilkCow.INI b/Art/Animations/Resources/MilkCow/MilkCow.INI new file mode 100644 index 00000000..7ca44f28 --- /dev/null +++ b/Art/Animations/Resources/MilkCow/MilkCow.INI @@ -0,0 +1,74 @@ +[Speed] +Normal Speed=62 +Fast Speed=62 + +[Animations] +BLANK= +DEFAULT=MilkCow.flc +RUN= +ATTACK1=MilkCow.flc +ATTACK2= +ATTACK3= +DEATH= +FORTIFY= +FIDGET= +VICTORY= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +JUNGLE= +FOREST= +PLANT= +STOP_AT_LAST_FRAME= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEATH=0.500000 +FORTIFY=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PLANT=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +FORTIFY= +FIDGET= +VICTORY= +BUILD= +ROAD= +MINE= +IRRIGATE= +JUNGLE= +FOREST= +FORTRESS= +CAPTURE= +PLANT= +STOP_AT_LAST_FRAME= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Resources/MilkCow/MilkCow.flc b/Art/Animations/Resources/MilkCow/MilkCow.flc new file mode 100644 index 00000000..d8afba62 Binary files /dev/null and b/Art/Animations/Resources/MilkCow/MilkCow.flc differ diff --git a/Art/Animations/Resources/MilkCow/MilkCow_night.INI b/Art/Animations/Resources/MilkCow/MilkCow_night.INI new file mode 100644 index 00000000..2542052d --- /dev/null +++ b/Art/Animations/Resources/MilkCow/MilkCow_night.INI @@ -0,0 +1,74 @@ +[Speed] +Normal Speed=62 +Fast Speed=62 + +[Animations] +BLANK= +DEFAULT=MilkCow_night.flc +RUN= +ATTACK1=MilkCow_night.flc +ATTACK2= +ATTACK3= +DEATH= +FORTIFY= +FIDGET= +VICTORY= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +JUNGLE= +FOREST= +PLANT= +STOP_AT_LAST_FRAME= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEATH=0.500000 +FORTIFY=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PLANT=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +FORTIFY= +FIDGET= +VICTORY= +BUILD= +ROAD= +MINE= +IRRIGATE= +JUNGLE= +FOREST= +FORTRESS= +CAPTURE= +PLANT= +STOP_AT_LAST_FRAME= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Resources/MilkCow/MilkCow_night.flc b/Art/Animations/Resources/MilkCow/MilkCow_night.flc new file mode 100644 index 00000000..4040046e Binary files /dev/null and b/Art/Animations/Resources/MilkCow/MilkCow_night.flc differ diff --git a/Art/Animations/Resources/MilkCow/Read Me.txt b/Art/Animations/Resources/MilkCow/Read Me.txt new file mode 100644 index 00000000..065d2719 --- /dev/null +++ b/Art/Animations/Resources/MilkCow/Read Me.txt @@ -0,0 +1,11 @@ +Unit: Milk Cow by Vuldacon March 26, 2014 + +Enjoy... + +Cheers, +Vuldacon + + +reduced from Wotan49 + +for C3X Districts diff --git a/Art/Animations/Resources/Whale/Whale.INI b/Art/Animations/Resources/Whale/Whale.INI new file mode 100644 index 00000000..bdab4107 --- /dev/null +++ b/Art/Animations/Resources/Whale/Whale.INI @@ -0,0 +1,91 @@ +[Speed] +Normal Speed=150 +Fast Speed=150 + +[Animations] +BLANK= +DEFAULT=Whale.flc +WALK= +RUN= +ATTACK1=Whale.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= + +[Timing] +BLANK=0.500000 +DEFAULT=0.500000 +WALK=0.500000 +RUN=0.500000 +ATTACK1=0.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Resources/Whale/Whale.flc b/Art/Animations/Resources/Whale/Whale.flc new file mode 100644 index 00000000..133d4aee Binary files /dev/null and b/Art/Animations/Resources/Whale/Whale.flc differ diff --git a/Art/Animations/Terrain/DeltaRivers/DeltaRivers_12.INI b/Art/Animations/Terrain/DeltaRivers/DeltaRivers_12.INI new file mode 100644 index 00000000..8f720dde --- /dev/null +++ b/Art/Animations/Terrain/DeltaRivers/DeltaRivers_12.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=DeltaRivers_12.flc +WALK= +RUN= +ATTACK1=DeltaRivers_12.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=10.500000 +DEFAULT=0.500000 +WALK=10.500000 +RUN=0.500000 +ATTACK1=10.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Terrain/DeltaRivers/DeltaRivers_12.flc b/Art/Animations/Terrain/DeltaRivers/DeltaRivers_12.flc new file mode 100644 index 00000000..8d86e992 Binary files /dev/null and b/Art/Animations/Terrain/DeltaRivers/DeltaRivers_12.flc differ diff --git a/Art/Animations/Terrain/Snow/Dev/sora_snow.mp4 b/Art/Animations/Terrain/Snow/Dev/sora_snow.mp4 new file mode 100644 index 00000000..0cb1556a Binary files /dev/null and b/Art/Animations/Terrain/Snow/Dev/sora_snow.mp4 differ diff --git a/Art/Animations/Terrain/Snow/Snow.INI b/Art/Animations/Terrain/Snow/Snow.INI new file mode 100644 index 00000000..45e525fe --- /dev/null +++ b/Art/Animations/Terrain/Snow/Snow.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=Snow.flc +WALK= +RUN= +ATTACK1=Snow.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=10.500000 +DEFAULT=0.500000 +WALK=10.500000 +RUN=0.500000 +ATTACK1=10.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Terrain/Snow/Snow.flc b/Art/Animations/Terrain/Snow/Snow.flc new file mode 100644 index 00000000..f2624b12 Binary files /dev/null and b/Art/Animations/Terrain/Snow/Snow.flc differ diff --git a/Art/Animations/Terrain/Snow/Snow.pcx b/Art/Animations/Terrain/Snow/Snow.pcx new file mode 100644 index 00000000..8ee84fe3 Binary files /dev/null and b/Art/Animations/Terrain/Snow/Snow.pcx differ diff --git a/Art/Animations/Terrain/Wave/Dev/Wave_orig_edit.pcx b/Art/Animations/Terrain/Wave/Dev/Wave_orig_edit.pcx new file mode 100644 index 00000000..6ed51e05 Binary files /dev/null and b/Art/Animations/Terrain/Wave/Dev/Wave_orig_edit.pcx differ diff --git a/Art/Animations/Terrain/Wave/Dev/Wave_raw_orig.pcx b/Art/Animations/Terrain/Wave/Dev/Wave_raw_orig.pcx new file mode 100644 index 00000000..86ac2c41 Binary files /dev/null and b/Art/Animations/Terrain/Wave/Dev/Wave_raw_orig.pcx differ diff --git a/Art/Animations/Terrain/Wave/Dev/Wave_raw_orig_crop.pcx b/Art/Animations/Terrain/Wave/Dev/Wave_raw_orig_crop.pcx new file mode 100644 index 00000000..9df76cb7 Binary files /dev/null and b/Art/Animations/Terrain/Wave/Dev/Wave_raw_orig_crop.pcx differ diff --git a/Art/Animations/Terrain/Wave/Dev/sora_wave.mp4 b/Art/Animations/Terrain/Wave/Dev/sora_wave.mp4 new file mode 100644 index 00000000..531ea70d Binary files /dev/null and b/Art/Animations/Terrain/Wave/Dev/sora_wave.mp4 differ diff --git a/Art/Animations/Terrain/Wave/Wave.INI b/Art/Animations/Terrain/Wave/Wave.INI new file mode 100644 index 00000000..c93d9af4 --- /dev/null +++ b/Art/Animations/Terrain/Wave/Wave.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=Wave.flc +WALK= +RUN= +ATTACK1=Wave.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=10.500000 +DEFAULT=0.500000 +WALK=10.500000 +RUN=0.500000 +ATTACK1=10.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Terrain/Wave/Wave.flc b/Art/Animations/Terrain/Wave/Wave.flc new file mode 100644 index 00000000..7dca6e31 Binary files /dev/null and b/Art/Animations/Terrain/Wave/Wave.flc differ diff --git a/Art/Animations/Terrain/Wave/Wave_night.INI b/Art/Animations/Terrain/Wave/Wave_night.INI new file mode 100644 index 00000000..f25fe0be --- /dev/null +++ b/Art/Animations/Terrain/Wave/Wave_night.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=Wave_night.flc +WALK= +RUN= +ATTACK1=Wave_night.flc +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= + +[Timing] +BLANK=10.500000 +DEFAULT=0.500000 +WALK=10.500000 +RUN=0.500000 +ATTACK1=10.500000 +ATTACK2=0.500000 +ATTACK3=0.500000 +DEFEND=0.500000 +DEATH=0.500000 +DEAD=0.500000 +FORTIFY=0.500000 +FORTIFYHOLD=0.500000 +FIDGET=0.500000 +VICTORY=0.500000 +TURNLEFT=0.500000 +TURNRIGHT=0.500000 +BUILD=0.500000 +ROAD=0.500000 +MINE=0.500000 +IRRIGATE=0.500000 +FORTRESS=0.500000 +CAPTURE=0.500000 +STOP_AT_LAST_FRAME=0.500000 +PauseROAD=0.500000 +PauseMINE=0.500000 +PauseIRRIGATE=0.500000 +JUNGLE=0.500000 +FOREST=0.500000 +PauseFOREST=0.500000 + +[Sound Effects] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1= +ATTACK2= +ATTACK3= +DEFEND= +DEATH= +DEAD= +FORTIFY= +FORTIFYHOLD= +FIDGET= +VICTORY= +TURNLEFT= +TURNRIGHT= +BUILD= +ROAD= +MINE= +IRRIGATE= +FORTRESS= +CAPTURE= +STOP_AT_LAST_FRAME= +PauseROAD= +PauseMINE= +PauseIRRIGATE= +JUNGLE= +FOREST= +PauseFOREST= +[Version] +VERSION=1 +[Palette] +PALETTE= diff --git a/Art/Animations/Terrain/Wave/Wave_night.flc b/Art/Animations/Terrain/Wave/Wave_night.flc new file mode 100644 index 00000000..258b5a49 Binary files /dev/null and b/Art/Animations/Terrain/Wave/Wave_night.flc differ diff --git a/Art/DayNight/.gitignore b/Art/DayNight/Fall/.gitignore similarity index 100% rename from Art/DayNight/.gitignore rename to Art/DayNight/Fall/.gitignore diff --git a/Art/Districts/1200/.gitignore b/Art/DayNight/Fall/Annotations/.gitignore similarity index 100% rename from Art/Districts/1200/.gitignore rename to Art/DayNight/Fall/Annotations/.gitignore diff --git a/Art/DayNight/Spring/.gitignore b/Art/DayNight/Spring/.gitignore new file mode 100644 index 00000000..37ab4d22 --- /dev/null +++ b/Art/DayNight/Spring/.gitignore @@ -0,0 +1,24 @@ +0100/ +0200/ +0300/ +0400/ +0500/ +0600/ +0700/ +0800/ +0900/ +1000/ +1100/ +1200/ +1300/ +1400/ +1500/ +1600/ +1700/ +1800/ +1900/ +2000/ +2100/ +2200/ +2300/ +2400/ \ No newline at end of file diff --git a/Art/DayNight/Spring/Annotations/.gitignore b/Art/DayNight/Spring/Annotations/.gitignore new file mode 100644 index 00000000..3a7f50b6 --- /dev/null +++ b/Art/DayNight/Spring/Annotations/.gitignore @@ -0,0 +1 @@ +*_lights.pcx \ No newline at end of file diff --git a/Art/.gitignore b/Art/DayNight/Summer/.gitignore similarity index 93% rename from Art/.gitignore rename to Art/DayNight/Summer/.gitignore index 5ab70449..9bbba50d 100644 --- a/Art/.gitignore +++ b/Art/DayNight/Summer/.gitignore @@ -1,4 +1,3 @@ -Seasons/ 0100/ 0200/ 0300/ diff --git a/Art/DayNight/Summer/1200/.gitignore b/Art/DayNight/Summer/1200/.gitignore new file mode 100644 index 00000000..3a7f50b6 --- /dev/null +++ b/Art/DayNight/Summer/1200/.gitignore @@ -0,0 +1 @@ +*_lights.pcx \ No newline at end of file diff --git a/Art/DayNight/Summer/1200/AMERWALL.PCX b/Art/DayNight/Summer/1200/AMERWALL.PCX new file mode 100644 index 00000000..524fed24 Binary files /dev/null and b/Art/DayNight/Summer/1200/AMERWALL.PCX differ diff --git a/Art/DayNight/Summer/1200/ASIANWALL.PCX b/Art/DayNight/Summer/1200/ASIANWALL.PCX new file mode 100644 index 00000000..8a5c390f Binary files /dev/null and b/Art/DayNight/Summer/1200/ASIANWALL.PCX differ diff --git a/Art/DayNight/Summer/1200/DESTROY.PCX b/Art/DayNight/Summer/1200/DESTROY.PCX new file mode 100644 index 00000000..d1892c99 Binary files /dev/null and b/Art/DayNight/Summer/1200/DESTROY.PCX differ diff --git a/Art/DayNight/Summer/1200/EUROWALL.PCX b/Art/DayNight/Summer/1200/EUROWALL.PCX new file mode 100644 index 00000000..11584d89 Binary files /dev/null and b/Art/DayNight/Summer/1200/EUROWALL.PCX differ diff --git a/Art/DayNight/Summer/1200/LMForests.pcx b/Art/DayNight/Summer/1200/LMForests.pcx new file mode 100644 index 00000000..130b45f2 Binary files /dev/null and b/Art/DayNight/Summer/1200/LMForests.pcx differ diff --git a/Art/DayNight/Summer/1200/LMHills.pcx b/Art/DayNight/Summer/1200/LMHills.pcx new file mode 100644 index 00000000..a0f680cf Binary files /dev/null and b/Art/DayNight/Summer/1200/LMHills.pcx differ diff --git a/Art/DayNight/Summer/1200/LMMountains.pcx b/Art/DayNight/Summer/1200/LMMountains.pcx new file mode 100644 index 00000000..36e08400 Binary files /dev/null and b/Art/DayNight/Summer/1200/LMMountains.pcx differ diff --git a/Art/DayNight/Summer/1200/MIDEASTWALL.PCX b/Art/DayNight/Summer/1200/MIDEASTWALL.PCX new file mode 100644 index 00000000..7a29ebd2 Binary files /dev/null and b/Art/DayNight/Summer/1200/MIDEASTWALL.PCX differ diff --git a/Art/DayNight/Summer/1200/Mountains-snow.pcx b/Art/DayNight/Summer/1200/Mountains-snow.pcx new file mode 100644 index 00000000..a8a928bb Binary files /dev/null and b/Art/DayNight/Summer/1200/Mountains-snow.pcx differ diff --git a/Art/DayNight/Summer/1200/Mountains.pcx b/Art/DayNight/Summer/1200/Mountains.pcx new file mode 100644 index 00000000..36e08400 Binary files /dev/null and b/Art/DayNight/Summer/1200/Mountains.pcx differ diff --git a/Art/DayNight/Summer/1200/ROMANWALL.PCX b/Art/DayNight/Summer/1200/ROMANWALL.PCX new file mode 100644 index 00000000..cd42618c Binary files /dev/null and b/Art/DayNight/Summer/1200/ROMANWALL.PCX differ diff --git a/Art/DayNight/Summer/1200/StartLoc.pcx b/Art/DayNight/Summer/1200/StartLoc.pcx new file mode 100644 index 00000000..c6c8bb9e Binary files /dev/null and b/Art/DayNight/Summer/1200/StartLoc.pcx differ diff --git a/Art/DayNight/Summer/1200/TerrainBuildings.PCX b/Art/DayNight/Summer/1200/TerrainBuildings.PCX new file mode 100644 index 00000000..4e5036cc Binary files /dev/null and b/Art/DayNight/Summer/1200/TerrainBuildings.PCX differ diff --git a/Art/DayNight/Summer/1200/Volcanos forests.pcx b/Art/DayNight/Summer/1200/Volcanos forests.pcx new file mode 100644 index 00000000..89f6986f Binary files /dev/null and b/Art/DayNight/Summer/1200/Volcanos forests.pcx differ diff --git a/Art/DayNight/Summer/1200/Volcanos jungles.pcx b/Art/DayNight/Summer/1200/Volcanos jungles.pcx new file mode 100644 index 00000000..dec00ae8 Binary files /dev/null and b/Art/DayNight/Summer/1200/Volcanos jungles.pcx differ diff --git a/Art/DayNight/Summer/1200/Volcanos-snow.pcx b/Art/DayNight/Summer/1200/Volcanos-snow.pcx new file mode 100644 index 00000000..75c888e2 Binary files /dev/null and b/Art/DayNight/Summer/1200/Volcanos-snow.pcx differ diff --git a/Art/DayNight/Summer/1200/Volcanos.pcx b/Art/DayNight/Summer/1200/Volcanos.pcx new file mode 100644 index 00000000..8f489d8e Binary files /dev/null and b/Art/DayNight/Summer/1200/Volcanos.pcx differ diff --git a/Art/DayNight/Summer/1200/craters.pcx b/Art/DayNight/Summer/1200/craters.pcx new file mode 100644 index 00000000..c34f8ec0 Binary files /dev/null and b/Art/DayNight/Summer/1200/craters.pcx differ diff --git a/Art/DayNight/Summer/1200/deltaRivers.pcx b/Art/DayNight/Summer/1200/deltaRivers.pcx new file mode 100644 index 00000000..12c30db0 Binary files /dev/null and b/Art/DayNight/Summer/1200/deltaRivers.pcx differ diff --git a/Art/DayNight/Summer/1200/floodplains.pcx b/Art/DayNight/Summer/1200/floodplains.pcx new file mode 100644 index 00000000..19e2bbde Binary files /dev/null and b/Art/DayNight/Summer/1200/floodplains.pcx differ diff --git a/Art/DayNight/Summer/1200/goodyhuts.pcx b/Art/DayNight/Summer/1200/goodyhuts.pcx new file mode 100644 index 00000000..3558d243 Binary files /dev/null and b/Art/DayNight/Summer/1200/goodyhuts.pcx differ diff --git a/Art/DayNight/Summer/1200/grassland forests.pcx b/Art/DayNight/Summer/1200/grassland forests.pcx new file mode 100644 index 00000000..fa1eb305 Binary files /dev/null and b/Art/DayNight/Summer/1200/grassland forests.pcx differ diff --git a/Art/DayNight/Summer/1200/hill forests.pcx b/Art/DayNight/Summer/1200/hill forests.pcx new file mode 100644 index 00000000..0dbc7a11 Binary files /dev/null and b/Art/DayNight/Summer/1200/hill forests.pcx differ diff --git a/Art/DayNight/Summer/1200/hill jungle.pcx b/Art/DayNight/Summer/1200/hill jungle.pcx new file mode 100644 index 00000000..3bdc01e1 Binary files /dev/null and b/Art/DayNight/Summer/1200/hill jungle.pcx differ diff --git a/Art/DayNight/Summer/1200/irrigation DESETT.pcx b/Art/DayNight/Summer/1200/irrigation DESETT.pcx new file mode 100644 index 00000000..ea26a30d Binary files /dev/null and b/Art/DayNight/Summer/1200/irrigation DESETT.pcx differ diff --git a/Art/DayNight/Summer/1200/irrigation PLAINS.pcx b/Art/DayNight/Summer/1200/irrigation PLAINS.pcx new file mode 100644 index 00000000..aeb4aefd Binary files /dev/null and b/Art/DayNight/Summer/1200/irrigation PLAINS.pcx differ diff --git a/Art/DayNight/Summer/1200/irrigation TUNDRA.pcx b/Art/DayNight/Summer/1200/irrigation TUNDRA.pcx new file mode 100644 index 00000000..91d1e5ff Binary files /dev/null and b/Art/DayNight/Summer/1200/irrigation TUNDRA.pcx differ diff --git a/Art/DayNight/Summer/1200/irrigation.pcx b/Art/DayNight/Summer/1200/irrigation.pcx new file mode 100644 index 00000000..1aced7cb Binary files /dev/null and b/Art/DayNight/Summer/1200/irrigation.pcx differ diff --git a/Art/DayNight/Summer/1200/landmark_terrain.pcx b/Art/DayNight/Summer/1200/landmark_terrain.pcx new file mode 100644 index 00000000..650f7ae2 Binary files /dev/null and b/Art/DayNight/Summer/1200/landmark_terrain.pcx differ diff --git a/Art/DayNight/Summer/1200/lwCSO.pcx b/Art/DayNight/Summer/1200/lwCSO.pcx new file mode 100644 index 00000000..e88578c5 Binary files /dev/null and b/Art/DayNight/Summer/1200/lwCSO.pcx differ diff --git a/Art/DayNight/Summer/1200/lwOOO.pcx b/Art/DayNight/Summer/1200/lwOOO.pcx new file mode 100644 index 00000000..f33ccf0a Binary files /dev/null and b/Art/DayNight/Summer/1200/lwOOO.pcx differ diff --git a/Art/DayNight/Summer/1200/lwSSS.pcx b/Art/DayNight/Summer/1200/lwSSS.pcx new file mode 100644 index 00000000..8fa7489e Binary files /dev/null and b/Art/DayNight/Summer/1200/lwSSS.pcx differ diff --git a/Art/DayNight/Summer/1200/lxdgc.pcx b/Art/DayNight/Summer/1200/lxdgc.pcx new file mode 100644 index 00000000..88a41cac Binary files /dev/null and b/Art/DayNight/Summer/1200/lxdgc.pcx differ diff --git a/Art/DayNight/Summer/1200/lxdgp.pcx b/Art/DayNight/Summer/1200/lxdgp.pcx new file mode 100644 index 00000000..bae97067 Binary files /dev/null and b/Art/DayNight/Summer/1200/lxdgp.pcx differ diff --git a/Art/DayNight/Summer/1200/lxdpc.pcx b/Art/DayNight/Summer/1200/lxdpc.pcx new file mode 100644 index 00000000..856eb067 Binary files /dev/null and b/Art/DayNight/Summer/1200/lxdpc.pcx differ diff --git a/Art/DayNight/Summer/1200/lxggc.pcx b/Art/DayNight/Summer/1200/lxggc.pcx new file mode 100644 index 00000000..f0d2c415 Binary files /dev/null and b/Art/DayNight/Summer/1200/lxggc.pcx differ diff --git a/Art/DayNight/Summer/1200/lxpgc.pcx b/Art/DayNight/Summer/1200/lxpgc.pcx new file mode 100644 index 00000000..bee89452 Binary files /dev/null and b/Art/DayNight/Summer/1200/lxpgc.pcx differ diff --git a/Art/DayNight/Summer/1200/lxtgc.pcx b/Art/DayNight/Summer/1200/lxtgc.pcx new file mode 100644 index 00000000..57c9743f Binary files /dev/null and b/Art/DayNight/Summer/1200/lxtgc.pcx differ diff --git a/Art/DayNight/Summer/1200/marsh.pcx b/Art/DayNight/Summer/1200/marsh.pcx new file mode 100644 index 00000000..a4da43ee Binary files /dev/null and b/Art/DayNight/Summer/1200/marsh.pcx differ diff --git a/Art/DayNight/Summer/1200/mountain forests.pcx b/Art/DayNight/Summer/1200/mountain forests.pcx new file mode 100644 index 00000000..21adc835 Binary files /dev/null and b/Art/DayNight/Summer/1200/mountain forests.pcx differ diff --git a/Art/DayNight/Summer/1200/mountain jungles.pcx b/Art/DayNight/Summer/1200/mountain jungles.pcx new file mode 100644 index 00000000..af0cb519 Binary files /dev/null and b/Art/DayNight/Summer/1200/mountain jungles.pcx differ diff --git a/Art/DayNight/Summer/1200/mtnRivers.pcx b/Art/DayNight/Summer/1200/mtnRivers.pcx new file mode 100644 index 00000000..3c765884 Binary files /dev/null and b/Art/DayNight/Summer/1200/mtnRivers.pcx differ diff --git a/Art/DayNight/Summer/1200/plains forests.pcx b/Art/DayNight/Summer/1200/plains forests.pcx new file mode 100644 index 00000000..2acc3e04 Binary files /dev/null and b/Art/DayNight/Summer/1200/plains forests.pcx differ diff --git a/Art/DayNight/Summer/1200/polarICEcaps-final.pcx b/Art/DayNight/Summer/1200/polarICEcaps-final.pcx new file mode 100644 index 00000000..a085e885 Binary files /dev/null and b/Art/DayNight/Summer/1200/polarICEcaps-final.pcx differ diff --git a/Art/DayNight/Summer/1200/pollution.pcx b/Art/DayNight/Summer/1200/pollution.pcx new file mode 100644 index 00000000..044513ff Binary files /dev/null and b/Art/DayNight/Summer/1200/pollution.pcx differ diff --git a/Art/DayNight/Summer/1200/rAMER.PCX b/Art/DayNight/Summer/1200/rAMER.PCX new file mode 100644 index 00000000..bc97a3ed Binary files /dev/null and b/Art/DayNight/Summer/1200/rAMER.PCX differ diff --git a/Art/DayNight/Summer/1200/rASIAN.PCX b/Art/DayNight/Summer/1200/rASIAN.PCX new file mode 100644 index 00000000..0768a012 Binary files /dev/null and b/Art/DayNight/Summer/1200/rASIAN.PCX differ diff --git a/Art/DayNight/Summer/1200/rEURO.PCX b/Art/DayNight/Summer/1200/rEURO.PCX new file mode 100644 index 00000000..a44a9406 Binary files /dev/null and b/Art/DayNight/Summer/1200/rEURO.PCX differ diff --git a/Art/DayNight/Summer/1200/rMIDEAST.PCX b/Art/DayNight/Summer/1200/rMIDEAST.PCX new file mode 100644 index 00000000..73ab6a23 Binary files /dev/null and b/Art/DayNight/Summer/1200/rMIDEAST.PCX differ diff --git a/Art/DayNight/Summer/1200/rROMAN.PCX b/Art/DayNight/Summer/1200/rROMAN.PCX new file mode 100644 index 00000000..b50f5e93 Binary files /dev/null and b/Art/DayNight/Summer/1200/rROMAN.PCX differ diff --git a/Art/DayNight/Annotations/rROMAN_lights.PCX b/Art/DayNight/Summer/1200/rROMAN_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/rROMAN_lights.PCX rename to Art/DayNight/Summer/1200/rROMAN_lights.PCX diff --git a/Art/DayNight/Summer/1200/railroads.pcx b/Art/DayNight/Summer/1200/railroads.pcx new file mode 100644 index 00000000..e542fe8c Binary files /dev/null and b/Art/DayNight/Summer/1200/railroads.pcx differ diff --git a/Art/DayNight/Summer/1200/resources.pcx b/Art/DayNight/Summer/1200/resources.pcx new file mode 100644 index 00000000..13b6ff3c Binary files /dev/null and b/Art/DayNight/Summer/1200/resources.pcx differ diff --git a/Art/DayNight/Summer/1200/roads.pcx b/Art/DayNight/Summer/1200/roads.pcx new file mode 100644 index 00000000..f0384d8a Binary files /dev/null and b/Art/DayNight/Summer/1200/roads.pcx differ diff --git a/Art/DayNight/Summer/1200/tnt.pcx b/Art/DayNight/Summer/1200/tnt.pcx new file mode 100644 index 00000000..a9c947a7 Binary files /dev/null and b/Art/DayNight/Summer/1200/tnt.pcx differ diff --git a/Art/DayNight/Summer/1200/tundra forests.pcx b/Art/DayNight/Summer/1200/tundra forests.pcx new file mode 100644 index 00000000..8630d73e Binary files /dev/null and b/Art/DayNight/Summer/1200/tundra forests.pcx differ diff --git a/Art/DayNight/Summer/1200/wCSO.pcx b/Art/DayNight/Summer/1200/wCSO.pcx new file mode 100644 index 00000000..c3055344 Binary files /dev/null and b/Art/DayNight/Summer/1200/wCSO.pcx differ diff --git a/Art/DayNight/Summer/1200/wOOO.pcx b/Art/DayNight/Summer/1200/wOOO.pcx new file mode 100644 index 00000000..941b84df Binary files /dev/null and b/Art/DayNight/Summer/1200/wOOO.pcx differ diff --git a/Art/DayNight/Summer/1200/wSSS.pcx b/Art/DayNight/Summer/1200/wSSS.pcx new file mode 100644 index 00000000..b2044a7e Binary files /dev/null and b/Art/DayNight/Summer/1200/wSSS.pcx differ diff --git a/Art/DayNight/Summer/1200/waterfalls.pcx b/Art/DayNight/Summer/1200/waterfalls.pcx new file mode 100644 index 00000000..0b1ccbdd Binary files /dev/null and b/Art/DayNight/Summer/1200/waterfalls.pcx differ diff --git a/Art/DayNight/Summer/1200/x_airfields and detect.PCX b/Art/DayNight/Summer/1200/x_airfields and detect.PCX new file mode 100644 index 00000000..997ac675 Binary files /dev/null and b/Art/DayNight/Summer/1200/x_airfields and detect.PCX differ diff --git a/Art/DayNight/Annotations/x_airfields and detect_lights.PCX b/Art/DayNight/Summer/1200/x_airfields and detect_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/x_airfields and detect_lights.PCX rename to Art/DayNight/Summer/1200/x_airfields and detect_lights.PCX diff --git a/Art/DayNight/Summer/1200/x_victory.pcx b/Art/DayNight/Summer/1200/x_victory.pcx new file mode 100644 index 00000000..9a26935f Binary files /dev/null and b/Art/DayNight/Summer/1200/x_victory.pcx differ diff --git a/Art/DayNight/Summer/1200/xdgc.pcx b/Art/DayNight/Summer/1200/xdgc.pcx new file mode 100644 index 00000000..8dec36c1 Binary files /dev/null and b/Art/DayNight/Summer/1200/xdgc.pcx differ diff --git a/Art/DayNight/Summer/1200/xdgp.pcx b/Art/DayNight/Summer/1200/xdgp.pcx new file mode 100644 index 00000000..8f4b35f9 Binary files /dev/null and b/Art/DayNight/Summer/1200/xdgp.pcx differ diff --git a/Art/DayNight/Summer/1200/xdpc.pcx b/Art/DayNight/Summer/1200/xdpc.pcx new file mode 100644 index 00000000..6f70100e Binary files /dev/null and b/Art/DayNight/Summer/1200/xdpc.pcx differ diff --git a/Art/DayNight/Summer/1200/xggc.pcx b/Art/DayNight/Summer/1200/xggc.pcx new file mode 100644 index 00000000..c2d46dfc Binary files /dev/null and b/Art/DayNight/Summer/1200/xggc.pcx differ diff --git a/Art/DayNight/Summer/1200/xhills.pcx b/Art/DayNight/Summer/1200/xhills.pcx new file mode 100644 index 00000000..a0f680cf Binary files /dev/null and b/Art/DayNight/Summer/1200/xhills.pcx differ diff --git a/Art/DayNight/Summer/1200/xpgc.pcx b/Art/DayNight/Summer/1200/xpgc.pcx new file mode 100644 index 00000000..80ef1e98 Binary files /dev/null and b/Art/DayNight/Summer/1200/xpgc.pcx differ diff --git a/Art/DayNight/Summer/1200/xtgc.pcx b/Art/DayNight/Summer/1200/xtgc.pcx new file mode 100644 index 00000000..d117b89e Binary files /dev/null and b/Art/DayNight/Summer/1200/xtgc.pcx differ diff --git a/Art/DayNight/Annotations/AMERWALL_lights.PCX b/Art/DayNight/Summer/Annotations/AMERWALL_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/AMERWALL_lights.PCX rename to Art/DayNight/Summer/Annotations/AMERWALL_lights.PCX diff --git a/Art/DayNight/Annotations/ASIANWALL_lights.PCX b/Art/DayNight/Summer/Annotations/ASIANWALL_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/ASIANWALL_lights.PCX rename to Art/DayNight/Summer/Annotations/ASIANWALL_lights.PCX diff --git a/Art/DayNight/Annotations/EUROWALL_lights.PCX b/Art/DayNight/Summer/Annotations/EUROWALL_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/EUROWALL_lights.PCX rename to Art/DayNight/Summer/Annotations/EUROWALL_lights.PCX diff --git a/Art/DayNight/Annotations/MIDEASTWALL_lights.PCX b/Art/DayNight/Summer/Annotations/MIDEASTWALL_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/MIDEASTWALL_lights.PCX rename to Art/DayNight/Summer/Annotations/MIDEASTWALL_lights.PCX diff --git a/Art/DayNight/Annotations/ROMANWALL_lights.PCX b/Art/DayNight/Summer/Annotations/ROMANWALL_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/ROMANWALL_lights.PCX rename to Art/DayNight/Summer/Annotations/ROMANWALL_lights.PCX diff --git a/Art/DayNight/Annotations/TerrainBuildings_lights.PCX b/Art/DayNight/Summer/Annotations/TerrainBuildings_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/TerrainBuildings_lights.PCX rename to Art/DayNight/Summer/Annotations/TerrainBuildings_lights.PCX diff --git a/Art/DayNight/Annotations/Volcanos forests_lights.pcx b/Art/DayNight/Summer/Annotations/Volcanos forests_lights.pcx similarity index 100% rename from Art/DayNight/Annotations/Volcanos forests_lights.pcx rename to Art/DayNight/Summer/Annotations/Volcanos forests_lights.pcx diff --git a/Art/DayNight/Annotations/Volcanos jungles_lights.pcx b/Art/DayNight/Summer/Annotations/Volcanos jungles_lights.pcx similarity index 100% rename from Art/DayNight/Annotations/Volcanos jungles_lights.pcx rename to Art/DayNight/Summer/Annotations/Volcanos jungles_lights.pcx diff --git a/Art/DayNight/Annotations/Volcanos_lights.pcx b/Art/DayNight/Summer/Annotations/Volcanos_lights.pcx similarity index 100% rename from Art/DayNight/Annotations/Volcanos_lights.pcx rename to Art/DayNight/Summer/Annotations/Volcanos_lights.pcx diff --git a/Art/DayNight/Annotations/goodyhuts_lights.pcx b/Art/DayNight/Summer/Annotations/goodyhuts_lights.pcx similarity index 100% rename from Art/DayNight/Annotations/goodyhuts_lights.pcx rename to Art/DayNight/Summer/Annotations/goodyhuts_lights.pcx diff --git a/Art/DayNight/Annotations/rAMER_lights.PCX b/Art/DayNight/Summer/Annotations/rAMER_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/rAMER_lights.PCX rename to Art/DayNight/Summer/Annotations/rAMER_lights.PCX diff --git a/Art/DayNight/Annotations/rASIAN_lights.PCX b/Art/DayNight/Summer/Annotations/rASIAN_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/rASIAN_lights.PCX rename to Art/DayNight/Summer/Annotations/rASIAN_lights.PCX diff --git a/Art/DayNight/Annotations/rEURO_lights.PCX b/Art/DayNight/Summer/Annotations/rEURO_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/rEURO_lights.PCX rename to Art/DayNight/Summer/Annotations/rEURO_lights.PCX diff --git a/Art/DayNight/Annotations/rMIDEAST_lights.PCX b/Art/DayNight/Summer/Annotations/rMIDEAST_lights.PCX similarity index 100% rename from Art/DayNight/Annotations/rMIDEAST_lights.PCX rename to Art/DayNight/Summer/Annotations/rMIDEAST_lights.PCX diff --git a/Art/DayNight/Summer/Annotations/rROMAN_lights.PCX b/Art/DayNight/Summer/Annotations/rROMAN_lights.PCX new file mode 100644 index 00000000..7151fba8 Binary files /dev/null and b/Art/DayNight/Summer/Annotations/rROMAN_lights.PCX differ diff --git a/Art/DayNight/Annotations/railroads_lights.pcx b/Art/DayNight/Summer/Annotations/railroads_lights.pcx similarity index 100% rename from Art/DayNight/Annotations/railroads_lights.pcx rename to Art/DayNight/Summer/Annotations/railroads_lights.pcx diff --git a/Art/DayNight/Summer/Annotations/x_airfields and detect_lights.PCX b/Art/DayNight/Summer/Annotations/x_airfields and detect_lights.PCX new file mode 100644 index 00000000..ae6838a9 Binary files /dev/null and b/Art/DayNight/Summer/Annotations/x_airfields and detect_lights.PCX differ diff --git a/Art/DayNight/Winter/.gitignore b/Art/DayNight/Winter/.gitignore new file mode 100644 index 00000000..37ab4d22 --- /dev/null +++ b/Art/DayNight/Winter/.gitignore @@ -0,0 +1,24 @@ +0100/ +0200/ +0300/ +0400/ +0500/ +0600/ +0700/ +0800/ +0900/ +1000/ +1100/ +1200/ +1300/ +1400/ +1500/ +1600/ +1700/ +1800/ +1900/ +2000/ +2100/ +2200/ +2300/ +2400/ \ No newline at end of file diff --git a/Art/DayNight/Winter/Annotations/.gitignore b/Art/DayNight/Winter/Annotations/.gitignore new file mode 100644 index 00000000..3a7f50b6 --- /dev/null +++ b/Art/DayNight/Winter/Annotations/.gitignore @@ -0,0 +1 @@ +*_lights.pcx \ No newline at end of file diff --git a/Art/Districts/Fall/.gitignore b/Art/Districts/Fall/.gitignore new file mode 100644 index 00000000..37ab4d22 --- /dev/null +++ b/Art/Districts/Fall/.gitignore @@ -0,0 +1,24 @@ +0100/ +0200/ +0300/ +0400/ +0500/ +0600/ +0700/ +0800/ +0900/ +1000/ +1100/ +1200/ +1300/ +1400/ +1500/ +1600/ +1700/ +1800/ +1900/ +2000/ +2100/ +2200/ +2300/ +2400/ \ No newline at end of file diff --git a/Art/Districts/Fall/Annotations/.gitignore b/Art/Districts/Fall/Annotations/.gitignore new file mode 100644 index 00000000..3a7f50b6 --- /dev/null +++ b/Art/Districts/Fall/Annotations/.gitignore @@ -0,0 +1 @@ +*_lights.pcx \ No newline at end of file diff --git a/Art/Districts/Spring/.gitignore b/Art/Districts/Spring/.gitignore new file mode 100644 index 00000000..37ab4d22 --- /dev/null +++ b/Art/Districts/Spring/.gitignore @@ -0,0 +1,24 @@ +0100/ +0200/ +0300/ +0400/ +0500/ +0600/ +0700/ +0800/ +0900/ +1000/ +1100/ +1200/ +1300/ +1400/ +1500/ +1600/ +1700/ +1800/ +1900/ +2000/ +2100/ +2200/ +2300/ +2400/ \ No newline at end of file diff --git a/Art/Districts/Spring/Annotations/.gitignore b/Art/Districts/Spring/Annotations/.gitignore new file mode 100644 index 00000000..3a7f50b6 --- /dev/null +++ b/Art/Districts/Spring/Annotations/.gitignore @@ -0,0 +1 @@ +*_lights.pcx \ No newline at end of file diff --git a/Art/Districts/Summer/.gitignore b/Art/Districts/Summer/.gitignore new file mode 100644 index 00000000..9bbba50d --- /dev/null +++ b/Art/Districts/Summer/.gitignore @@ -0,0 +1,24 @@ +0100/ +0200/ +0300/ +0400/ +0500/ +0600/ +0700/ +0800/ +0900/ +1000/ +1100/ + +1300/ +1400/ +1500/ +1600/ +1700/ +1800/ +1900/ +2000/ +2100/ +2200/ +2300/ +2400/ \ No newline at end of file diff --git a/Art/Districts/Summer/1200/.gitignore b/Art/Districts/Summer/1200/.gitignore new file mode 100644 index 00000000..3a7f50b6 --- /dev/null +++ b/Art/Districts/Summer/1200/.gitignore @@ -0,0 +1 @@ +*_lights.pcx \ No newline at end of file diff --git a/Art/Districts/1200/Abandoned.PCX b/Art/Districts/Summer/1200/Abandoned.PCX similarity index 100% rename from Art/Districts/1200/Abandoned.PCX rename to Art/Districts/Summer/1200/Abandoned.PCX diff --git a/Art/Districts/1200/Aerodrome.PCX b/Art/Districts/Summer/1200/Aerodrome.PCX similarity index 100% rename from Art/Districts/1200/Aerodrome.PCX rename to Art/Districts/Summer/1200/Aerodrome.PCX diff --git a/Art/Districts/1200/Bridge.PCX b/Art/Districts/Summer/1200/Bridge.PCX similarity index 100% rename from Art/Districts/1200/Bridge.PCX rename to Art/Districts/Summer/1200/Bridge.PCX diff --git a/Art/Districts/1200/Campus.PCX b/Art/Districts/Summer/1200/Campus.PCX similarity index 100% rename from Art/Districts/1200/Campus.PCX rename to Art/Districts/Summer/1200/Campus.PCX diff --git a/Art/Districts/1200/Canal.PCX b/Art/Districts/Summer/1200/Canal.PCX similarity index 100% rename from Art/Districts/1200/Canal.PCX rename to Art/Districts/Summer/1200/Canal.PCX diff --git a/Art/Districts/1200/CentralRailHub_AMER.PCX b/Art/Districts/Summer/1200/CentralRailHub_AMER.PCX similarity index 100% rename from Art/Districts/1200/CentralRailHub_AMER.PCX rename to Art/Districts/Summer/1200/CentralRailHub_AMER.PCX diff --git a/Art/Districts/1200/CentralRailHub_ASIAN.PCX b/Art/Districts/Summer/1200/CentralRailHub_ASIAN.PCX similarity index 100% rename from Art/Districts/1200/CentralRailHub_ASIAN.PCX rename to Art/Districts/Summer/1200/CentralRailHub_ASIAN.PCX diff --git a/Art/Districts/1200/CentralRailHub_EURO.PCX b/Art/Districts/Summer/1200/CentralRailHub_EURO.PCX similarity index 100% rename from Art/Districts/1200/CentralRailHub_EURO.PCX rename to Art/Districts/Summer/1200/CentralRailHub_EURO.PCX diff --git a/Art/Districts/1200/CentralRailHub_MIDEAST.PCX b/Art/Districts/Summer/1200/CentralRailHub_MIDEAST.PCX similarity index 100% rename from Art/Districts/1200/CentralRailHub_MIDEAST.PCX rename to Art/Districts/Summer/1200/CentralRailHub_MIDEAST.PCX diff --git a/Art/Districts/1200/CentralRailHub_ROMAN.PCX b/Art/Districts/Summer/1200/CentralRailHub_ROMAN.PCX similarity index 100% rename from Art/Districts/1200/CentralRailHub_ROMAN.PCX rename to Art/Districts/Summer/1200/CentralRailHub_ROMAN.PCX diff --git a/Art/Districts/1200/CommercialHub_AMER.PCX b/Art/Districts/Summer/1200/CommercialHub_AMER.PCX similarity index 100% rename from Art/Districts/1200/CommercialHub_AMER.PCX rename to Art/Districts/Summer/1200/CommercialHub_AMER.PCX diff --git a/Art/Districts/1200/CommercialHub_ASIAN.PCX b/Art/Districts/Summer/1200/CommercialHub_ASIAN.PCX similarity index 100% rename from Art/Districts/1200/CommercialHub_ASIAN.PCX rename to Art/Districts/Summer/1200/CommercialHub_ASIAN.PCX diff --git a/Art/Districts/1200/CommercialHub_EURO.PCX b/Art/Districts/Summer/1200/CommercialHub_EURO.PCX similarity index 100% rename from Art/Districts/1200/CommercialHub_EURO.PCX rename to Art/Districts/Summer/1200/CommercialHub_EURO.PCX diff --git a/Art/Districts/1200/CommercialHub_MIDEAST.PCX b/Art/Districts/Summer/1200/CommercialHub_MIDEAST.PCX similarity index 100% rename from Art/Districts/1200/CommercialHub_MIDEAST.PCX rename to Art/Districts/Summer/1200/CommercialHub_MIDEAST.PCX diff --git a/Art/Districts/1200/CommercialHub_ROMAN.PCX b/Art/Districts/Summer/1200/CommercialHub_ROMAN.PCX similarity index 100% rename from Art/Districts/1200/CommercialHub_ROMAN.PCX rename to Art/Districts/Summer/1200/CommercialHub_ROMAN.PCX diff --git a/Art/Districts/1200/DataCenter.PCX b/Art/Districts/Summer/1200/DataCenter.PCX similarity index 100% rename from Art/Districts/1200/DataCenter.PCX rename to Art/Districts/Summer/1200/DataCenter.PCX diff --git a/Art/Districts/1200/DistributionHub.PCX b/Art/Districts/Summer/1200/DistributionHub.PCX similarity index 100% rename from Art/Districts/1200/DistributionHub.PCX rename to Art/Districts/Summer/1200/DistributionHub.PCX diff --git a/Art/Districts/1200/Encampment.PCX b/Art/Districts/Summer/1200/Encampment.PCX similarity index 100% rename from Art/Districts/1200/Encampment.PCX rename to Art/Districts/Summer/1200/Encampment.PCX diff --git a/Art/Districts/1200/EnergyGrid.PCX b/Art/Districts/Summer/1200/EnergyGrid.PCX similarity index 100% rename from Art/Districts/1200/EnergyGrid.PCX rename to Art/Districts/Summer/1200/EnergyGrid.PCX diff --git a/Art/Districts/1200/EntertainmentComplex_AMER.PCX b/Art/Districts/Summer/1200/EntertainmentComplex_AMER.PCX similarity index 100% rename from Art/Districts/1200/EntertainmentComplex_AMER.PCX rename to Art/Districts/Summer/1200/EntertainmentComplex_AMER.PCX diff --git a/Art/Districts/1200/EntertainmentComplex_ASIAN.PCX b/Art/Districts/Summer/1200/EntertainmentComplex_ASIAN.PCX similarity index 100% rename from Art/Districts/1200/EntertainmentComplex_ASIAN.PCX rename to Art/Districts/Summer/1200/EntertainmentComplex_ASIAN.PCX diff --git a/Art/Districts/1200/EntertainmentComplex_EURO.PCX b/Art/Districts/Summer/1200/EntertainmentComplex_EURO.PCX similarity index 100% rename from Art/Districts/1200/EntertainmentComplex_EURO.PCX rename to Art/Districts/Summer/1200/EntertainmentComplex_EURO.PCX diff --git a/Art/Districts/1200/EntertainmentComplex_MIDEAST.PCX b/Art/Districts/Summer/1200/EntertainmentComplex_MIDEAST.PCX similarity index 100% rename from Art/Districts/1200/EntertainmentComplex_MIDEAST.PCX rename to Art/Districts/Summer/1200/EntertainmentComplex_MIDEAST.PCX diff --git a/Art/Districts/1200/EntertainmentComplex_ROMAN.PCX b/Art/Districts/Summer/1200/EntertainmentComplex_ROMAN.PCX similarity index 100% rename from Art/Districts/1200/EntertainmentComplex_ROMAN.PCX rename to Art/Districts/Summer/1200/EntertainmentComplex_ROMAN.PCX diff --git a/Art/Districts/1200/GreatWall.pcx b/Art/Districts/Summer/1200/GreatWall.pcx similarity index 100% rename from Art/Districts/1200/GreatWall.pcx rename to Art/Districts/Summer/1200/GreatWall.pcx diff --git a/Art/Districts/1200/HolySite_AMER.PCX b/Art/Districts/Summer/1200/HolySite_AMER.PCX similarity index 100% rename from Art/Districts/1200/HolySite_AMER.PCX rename to Art/Districts/Summer/1200/HolySite_AMER.PCX diff --git a/Art/Districts/1200/HolySite_ASIAN.PCX b/Art/Districts/Summer/1200/HolySite_ASIAN.PCX similarity index 100% rename from Art/Districts/1200/HolySite_ASIAN.PCX rename to Art/Districts/Summer/1200/HolySite_ASIAN.PCX diff --git a/Art/Districts/1200/HolySite_EURO.PCX b/Art/Districts/Summer/1200/HolySite_EURO.PCX similarity index 100% rename from Art/Districts/1200/HolySite_EURO.PCX rename to Art/Districts/Summer/1200/HolySite_EURO.PCX diff --git a/Art/Districts/1200/HolySite_MIDEAST.PCX b/Art/Districts/Summer/1200/HolySite_MIDEAST.PCX similarity index 100% rename from Art/Districts/1200/HolySite_MIDEAST.PCX rename to Art/Districts/Summer/1200/HolySite_MIDEAST.PCX diff --git a/Art/Districts/1200/HolySite_ROMAN.PCX b/Art/Districts/Summer/1200/HolySite_ROMAN.PCX similarity index 100% rename from Art/Districts/1200/HolySite_ROMAN.PCX rename to Art/Districts/Summer/1200/HolySite_ROMAN.PCX diff --git a/Art/Districts/1200/HolySite_template.PCX b/Art/Districts/Summer/1200/HolySite_template.PCX similarity index 100% rename from Art/Districts/1200/HolySite_template.PCX rename to Art/Districts/Summer/1200/HolySite_template.PCX diff --git a/Art/Districts/1200/IndustrialZone.PCX b/Art/Districts/Summer/1200/IndustrialZone.PCX similarity index 100% rename from Art/Districts/1200/IndustrialZone.PCX rename to Art/Districts/Summer/1200/IndustrialZone.PCX diff --git a/Art/Districts/1200/NaturalWonders.pcx b/Art/Districts/Summer/1200/NaturalWonders.pcx similarity index 100% rename from Art/Districts/1200/NaturalWonders.pcx rename to Art/Districts/Summer/1200/NaturalWonders.pcx diff --git a/Art/Districts/1200/Neighborhood_AMER.PCX b/Art/Districts/Summer/1200/Neighborhood_AMER.PCX similarity index 100% rename from Art/Districts/1200/Neighborhood_AMER.PCX rename to Art/Districts/Summer/1200/Neighborhood_AMER.PCX diff --git a/Art/Districts/1200/Neighborhood_ASIAN.PCX b/Art/Districts/Summer/1200/Neighborhood_ASIAN.PCX similarity index 100% rename from Art/Districts/1200/Neighborhood_ASIAN.PCX rename to Art/Districts/Summer/1200/Neighborhood_ASIAN.PCX diff --git a/Art/Districts/1200/Neighborhood_EURO.PCX b/Art/Districts/Summer/1200/Neighborhood_EURO.PCX similarity index 100% rename from Art/Districts/1200/Neighborhood_EURO.PCX rename to Art/Districts/Summer/1200/Neighborhood_EURO.PCX diff --git a/Art/Districts/1200/Neighborhood_MIDEAST.PCX b/Art/Districts/Summer/1200/Neighborhood_MIDEAST.PCX similarity index 100% rename from Art/Districts/1200/Neighborhood_MIDEAST.PCX rename to Art/Districts/Summer/1200/Neighborhood_MIDEAST.PCX diff --git a/Art/Districts/1200/Neighborhood_ROMAN.PCX b/Art/Districts/Summer/1200/Neighborhood_ROMAN.PCX similarity index 100% rename from Art/Districts/1200/Neighborhood_ROMAN.PCX rename to Art/Districts/Summer/1200/Neighborhood_ROMAN.PCX diff --git a/Art/Districts/1200/OffshoreExtractionZone.PCX b/Art/Districts/Summer/1200/OffshoreExtractionZone.PCX similarity index 100% rename from Art/Districts/1200/OffshoreExtractionZone.PCX rename to Art/Districts/Summer/1200/OffshoreExtractionZone.PCX diff --git a/Art/Districts/1200/Park.PCX b/Art/Districts/Summer/1200/Park.PCX similarity index 100% rename from Art/Districts/1200/Park.PCX rename to Art/Districts/Summer/1200/Park.PCX diff --git a/Art/Districts/1200/Port_NE.PCX b/Art/Districts/Summer/1200/Port_NE.PCX similarity index 100% rename from Art/Districts/1200/Port_NE.PCX rename to Art/Districts/Summer/1200/Port_NE.PCX diff --git a/Art/Districts/1200/Port_NW.PCX b/Art/Districts/Summer/1200/Port_NW.PCX similarity index 100% rename from Art/Districts/1200/Port_NW.PCX rename to Art/Districts/Summer/1200/Port_NW.PCX diff --git a/Art/Districts/1200/Port_SE.PCX b/Art/Districts/Summer/1200/Port_SE.PCX similarity index 100% rename from Art/Districts/1200/Port_SE.PCX rename to Art/Districts/Summer/1200/Port_SE.PCX diff --git a/Art/Districts/1200/Port_SW.PCX b/Art/Districts/Summer/1200/Port_SW.PCX similarity index 100% rename from Art/Districts/1200/Port_SW.PCX rename to Art/Districts/Summer/1200/Port_SW.PCX diff --git a/Art/Districts/1200/SkiResort.PCX b/Art/Districts/Summer/1200/SkiResort.PCX similarity index 100% rename from Art/Districts/1200/SkiResort.PCX rename to Art/Districts/Summer/1200/SkiResort.PCX diff --git a/Art/Districts/1200/SmallWonders.pcx b/Art/Districts/Summer/1200/SmallWonders.pcx similarity index 100% rename from Art/Districts/1200/SmallWonders.pcx rename to Art/Districts/Summer/1200/SmallWonders.pcx diff --git a/Art/Districts/1200/WaterPark.pcx b/Art/Districts/Summer/1200/WaterPark.pcx similarity index 100% rename from Art/Districts/1200/WaterPark.pcx rename to Art/Districts/Summer/1200/WaterPark.pcx diff --git a/Art/Districts/1200/_template.PCX b/Art/Districts/Summer/1200/WindFarm.PCX similarity index 100% rename from Art/Districts/1200/_template.PCX rename to Art/Districts/Summer/1200/WindFarm.PCX diff --git a/Art/Districts/1200/WonderDistrict.PCX b/Art/Districts/Summer/1200/WonderDistrict.PCX similarity index 100% rename from Art/Districts/1200/WonderDistrict.PCX rename to Art/Districts/Summer/1200/WonderDistrict.PCX diff --git a/Art/Districts/1200/Wonders.PCX b/Art/Districts/Summer/1200/Wonders.PCX similarity index 100% rename from Art/Districts/1200/Wonders.PCX rename to Art/Districts/Summer/1200/Wonders.PCX diff --git a/Art/Districts/1200/Wonders_2.PCX b/Art/Districts/Summer/1200/Wonders_2.PCX similarity index 100% rename from Art/Districts/1200/Wonders_2.PCX rename to Art/Districts/Summer/1200/Wonders_2.PCX diff --git a/Art/Districts/1200/Wonders_3.PCX b/Art/Districts/Summer/1200/Wonders_3.PCX similarity index 100% rename from Art/Districts/1200/Wonders_3.PCX rename to Art/Districts/Summer/1200/Wonders_3.PCX diff --git a/Art/Districts/Summer/1200/_template.PCX b/Art/Districts/Summer/1200/_template.PCX new file mode 100644 index 00000000..33118518 Binary files /dev/null and b/Art/Districts/Summer/1200/_template.PCX differ diff --git a/Art/Districts/1200/_template_natural_wonders.pcx b/Art/Districts/Summer/1200/_template_natural_wonders.pcx similarity index 100% rename from Art/Districts/1200/_template_natural_wonders.pcx rename to Art/Districts/Summer/1200/_template_natural_wonders.pcx diff --git a/Art/Districts/Annotations/Aerodrome_lights.PCX b/Art/Districts/Summer/Annotations/Aerodrome_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Aerodrome_lights.PCX rename to Art/Districts/Summer/Annotations/Aerodrome_lights.PCX diff --git a/Art/Districts/Annotations/Bridge_lights.PCX b/Art/Districts/Summer/Annotations/Bridge_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Bridge_lights.PCX rename to Art/Districts/Summer/Annotations/Bridge_lights.PCX diff --git a/Art/Districts/Annotations/Campus_lights.PCX b/Art/Districts/Summer/Annotations/Campus_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Campus_lights.PCX rename to Art/Districts/Summer/Annotations/Campus_lights.PCX diff --git a/Art/Districts/Annotations/Canal_lights.PCX b/Art/Districts/Summer/Annotations/Canal_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Canal_lights.PCX rename to Art/Districts/Summer/Annotations/Canal_lights.PCX diff --git a/Art/Districts/Annotations/CentralRailHub_AMER_lights.PCX b/Art/Districts/Summer/Annotations/CentralRailHub_AMER_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CentralRailHub_AMER_lights.PCX rename to Art/Districts/Summer/Annotations/CentralRailHub_AMER_lights.PCX diff --git a/Art/Districts/Annotations/CentralRailHub_ASIAN_lights.PCX b/Art/Districts/Summer/Annotations/CentralRailHub_ASIAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CentralRailHub_ASIAN_lights.PCX rename to Art/Districts/Summer/Annotations/CentralRailHub_ASIAN_lights.PCX diff --git a/Art/Districts/Annotations/CentralRailHub_EURO_lights.PCX b/Art/Districts/Summer/Annotations/CentralRailHub_EURO_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CentralRailHub_EURO_lights.PCX rename to Art/Districts/Summer/Annotations/CentralRailHub_EURO_lights.PCX diff --git a/Art/Districts/Annotations/CentralRailHub_MIDEAST_lights.PCX b/Art/Districts/Summer/Annotations/CentralRailHub_MIDEAST_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CentralRailHub_MIDEAST_lights.PCX rename to Art/Districts/Summer/Annotations/CentralRailHub_MIDEAST_lights.PCX diff --git a/Art/Districts/Annotations/CentralRailHub_ROMAN_lights.PCX b/Art/Districts/Summer/Annotations/CentralRailHub_ROMAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CentralRailHub_ROMAN_lights.PCX rename to Art/Districts/Summer/Annotations/CentralRailHub_ROMAN_lights.PCX diff --git a/Art/Districts/Annotations/CommercialHub_AMER_lights.PCX b/Art/Districts/Summer/Annotations/CommercialHub_AMER_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CommercialHub_AMER_lights.PCX rename to Art/Districts/Summer/Annotations/CommercialHub_AMER_lights.PCX diff --git a/Art/Districts/Annotations/CommercialHub_ASIAN_lights.PCX b/Art/Districts/Summer/Annotations/CommercialHub_ASIAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CommercialHub_ASIAN_lights.PCX rename to Art/Districts/Summer/Annotations/CommercialHub_ASIAN_lights.PCX diff --git a/Art/Districts/Annotations/CommercialHub_EURO_lights.PCX b/Art/Districts/Summer/Annotations/CommercialHub_EURO_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CommercialHub_EURO_lights.PCX rename to Art/Districts/Summer/Annotations/CommercialHub_EURO_lights.PCX diff --git a/Art/Districts/Annotations/CommercialHub_MIDEAST_lights.PCX b/Art/Districts/Summer/Annotations/CommercialHub_MIDEAST_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CommercialHub_MIDEAST_lights.PCX rename to Art/Districts/Summer/Annotations/CommercialHub_MIDEAST_lights.PCX diff --git a/Art/Districts/Annotations/CommercialHub_ROMAN_lights.PCX b/Art/Districts/Summer/Annotations/CommercialHub_ROMAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/CommercialHub_ROMAN_lights.PCX rename to Art/Districts/Summer/Annotations/CommercialHub_ROMAN_lights.PCX diff --git a/Art/Districts/Annotations/DataCenter_lights.PCX b/Art/Districts/Summer/Annotations/DataCenter_lights.PCX similarity index 100% rename from Art/Districts/Annotations/DataCenter_lights.PCX rename to Art/Districts/Summer/Annotations/DataCenter_lights.PCX diff --git a/Art/Districts/Annotations/DistributionHub_lights.PCX b/Art/Districts/Summer/Annotations/DistributionHub_lights.PCX similarity index 100% rename from Art/Districts/Annotations/DistributionHub_lights.PCX rename to Art/Districts/Summer/Annotations/DistributionHub_lights.PCX diff --git a/Art/Districts/Annotations/Encampment_lights.PCX b/Art/Districts/Summer/Annotations/Encampment_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Encampment_lights.PCX rename to Art/Districts/Summer/Annotations/Encampment_lights.PCX diff --git a/Art/Districts/Annotations/EnergyGrid_lights.PCX b/Art/Districts/Summer/Annotations/EnergyGrid_lights.PCX similarity index 100% rename from Art/Districts/Annotations/EnergyGrid_lights.PCX rename to Art/Districts/Summer/Annotations/EnergyGrid_lights.PCX diff --git a/Art/Districts/Annotations/EntertainmentComplex_AMER_lights.PCX b/Art/Districts/Summer/Annotations/EntertainmentComplex_AMER_lights.PCX similarity index 100% rename from Art/Districts/Annotations/EntertainmentComplex_AMER_lights.PCX rename to Art/Districts/Summer/Annotations/EntertainmentComplex_AMER_lights.PCX diff --git a/Art/Districts/Annotations/EntertainmentComplex_ASIAN_lights.PCX b/Art/Districts/Summer/Annotations/EntertainmentComplex_ASIAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/EntertainmentComplex_ASIAN_lights.PCX rename to Art/Districts/Summer/Annotations/EntertainmentComplex_ASIAN_lights.PCX diff --git a/Art/Districts/Annotations/EntertainmentComplex_EURO_lights.PCX b/Art/Districts/Summer/Annotations/EntertainmentComplex_EURO_lights.PCX similarity index 100% rename from Art/Districts/Annotations/EntertainmentComplex_EURO_lights.PCX rename to Art/Districts/Summer/Annotations/EntertainmentComplex_EURO_lights.PCX diff --git a/Art/Districts/Annotations/EntertainmentComplex_MIDEAST_lights.PCX b/Art/Districts/Summer/Annotations/EntertainmentComplex_MIDEAST_lights.PCX similarity index 100% rename from Art/Districts/Annotations/EntertainmentComplex_MIDEAST_lights.PCX rename to Art/Districts/Summer/Annotations/EntertainmentComplex_MIDEAST_lights.PCX diff --git a/Art/Districts/Annotations/EntertainmentComplex_ROMAN_lights.PCX b/Art/Districts/Summer/Annotations/EntertainmentComplex_ROMAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/EntertainmentComplex_ROMAN_lights.PCX rename to Art/Districts/Summer/Annotations/EntertainmentComplex_ROMAN_lights.PCX diff --git a/Art/Districts/Annotations/GreatWall_lights.pcx b/Art/Districts/Summer/Annotations/GreatWall_lights.pcx similarity index 100% rename from Art/Districts/Annotations/GreatWall_lights.pcx rename to Art/Districts/Summer/Annotations/GreatWall_lights.pcx diff --git a/Art/Districts/Annotations/HolySite_AMER_lights.PCX b/Art/Districts/Summer/Annotations/HolySite_AMER_lights.PCX similarity index 100% rename from Art/Districts/Annotations/HolySite_AMER_lights.PCX rename to Art/Districts/Summer/Annotations/HolySite_AMER_lights.PCX diff --git a/Art/Districts/Annotations/HolySite_ASIAN_lights.PCX b/Art/Districts/Summer/Annotations/HolySite_ASIAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/HolySite_ASIAN_lights.PCX rename to Art/Districts/Summer/Annotations/HolySite_ASIAN_lights.PCX diff --git a/Art/Districts/Annotations/HolySite_EURO_lights.PCX b/Art/Districts/Summer/Annotations/HolySite_EURO_lights.PCX similarity index 100% rename from Art/Districts/Annotations/HolySite_EURO_lights.PCX rename to Art/Districts/Summer/Annotations/HolySite_EURO_lights.PCX diff --git a/Art/Districts/Annotations/HolySite_MIDEAST_lights.PCX b/Art/Districts/Summer/Annotations/HolySite_MIDEAST_lights.PCX similarity index 100% rename from Art/Districts/Annotations/HolySite_MIDEAST_lights.PCX rename to Art/Districts/Summer/Annotations/HolySite_MIDEAST_lights.PCX diff --git a/Art/Districts/Annotations/HolySite_ROMAN_lights.PCX b/Art/Districts/Summer/Annotations/HolySite_ROMAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/HolySite_ROMAN_lights.PCX rename to Art/Districts/Summer/Annotations/HolySite_ROMAN_lights.PCX diff --git a/Art/Districts/Annotations/IndustrialZone_lights.PCX b/Art/Districts/Summer/Annotations/IndustrialZone_lights.PCX similarity index 100% rename from Art/Districts/Annotations/IndustrialZone_lights.PCX rename to Art/Districts/Summer/Annotations/IndustrialZone_lights.PCX diff --git a/Art/Districts/Annotations/Neighborhood_AMER_lights.PCX b/Art/Districts/Summer/Annotations/Neighborhood_AMER_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Neighborhood_AMER_lights.PCX rename to Art/Districts/Summer/Annotations/Neighborhood_AMER_lights.PCX diff --git a/Art/Districts/Annotations/Neighborhood_ASIAN_lights.PCX b/Art/Districts/Summer/Annotations/Neighborhood_ASIAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Neighborhood_ASIAN_lights.PCX rename to Art/Districts/Summer/Annotations/Neighborhood_ASIAN_lights.PCX diff --git a/Art/Districts/Annotations/Neighborhood_EURO_lights.PCX b/Art/Districts/Summer/Annotations/Neighborhood_EURO_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Neighborhood_EURO_lights.PCX rename to Art/Districts/Summer/Annotations/Neighborhood_EURO_lights.PCX diff --git a/Art/Districts/Annotations/Neighborhood_MIDEAST_lights.PCX b/Art/Districts/Summer/Annotations/Neighborhood_MIDEAST_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Neighborhood_MIDEAST_lights.PCX rename to Art/Districts/Summer/Annotations/Neighborhood_MIDEAST_lights.PCX diff --git a/Art/Districts/Annotations/Neighborhood_ROMAN_lights.PCX b/Art/Districts/Summer/Annotations/Neighborhood_ROMAN_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Neighborhood_ROMAN_lights.PCX rename to Art/Districts/Summer/Annotations/Neighborhood_ROMAN_lights.PCX diff --git a/Art/Districts/Annotations/OffshoreExtractionZone_lights.PCX b/Art/Districts/Summer/Annotations/OffshoreExtractionZone_lights.PCX similarity index 100% rename from Art/Districts/Annotations/OffshoreExtractionZone_lights.PCX rename to Art/Districts/Summer/Annotations/OffshoreExtractionZone_lights.PCX diff --git a/Art/Districts/Annotations/Park_lights.PCX b/Art/Districts/Summer/Annotations/Park_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Park_lights.PCX rename to Art/Districts/Summer/Annotations/Park_lights.PCX diff --git a/Art/Districts/Annotations/Port_NE_lights.PCX b/Art/Districts/Summer/Annotations/Port_NE_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Port_NE_lights.PCX rename to Art/Districts/Summer/Annotations/Port_NE_lights.PCX diff --git a/Art/Districts/Annotations/Port_NW_lights.PCX b/Art/Districts/Summer/Annotations/Port_NW_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Port_NW_lights.PCX rename to Art/Districts/Summer/Annotations/Port_NW_lights.PCX diff --git a/Art/Districts/Annotations/Port_SE_lights.PCX b/Art/Districts/Summer/Annotations/Port_SE_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Port_SE_lights.PCX rename to Art/Districts/Summer/Annotations/Port_SE_lights.PCX diff --git a/Art/Districts/Annotations/Port_SW_lights.PCX b/Art/Districts/Summer/Annotations/Port_SW_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Port_SW_lights.PCX rename to Art/Districts/Summer/Annotations/Port_SW_lights.PCX diff --git a/Art/Districts/Annotations/SkiResort_lights.PCX b/Art/Districts/Summer/Annotations/SkiResort_lights.PCX similarity index 100% rename from Art/Districts/Annotations/SkiResort_lights.PCX rename to Art/Districts/Summer/Annotations/SkiResort_lights.PCX diff --git a/Art/Districts/Annotations/SmallWonders_lights.pcx b/Art/Districts/Summer/Annotations/SmallWonders_lights.pcx similarity index 100% rename from Art/Districts/Annotations/SmallWonders_lights.pcx rename to Art/Districts/Summer/Annotations/SmallWonders_lights.pcx diff --git a/Art/Districts/Annotations/WaterPark_lights.pcx b/Art/Districts/Summer/Annotations/WaterPark_lights.pcx similarity index 100% rename from Art/Districts/Annotations/WaterPark_lights.pcx rename to Art/Districts/Summer/Annotations/WaterPark_lights.pcx diff --git a/Art/Districts/Annotations/WonderDistrict_lights.PCX b/Art/Districts/Summer/Annotations/WonderDistrict_lights.PCX similarity index 100% rename from Art/Districts/Annotations/WonderDistrict_lights.PCX rename to Art/Districts/Summer/Annotations/WonderDistrict_lights.PCX diff --git a/Art/Districts/Annotations/Wonders_2_lights.PCX b/Art/Districts/Summer/Annotations/Wonders_2_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Wonders_2_lights.PCX rename to Art/Districts/Summer/Annotations/Wonders_2_lights.PCX diff --git a/Art/Districts/Annotations/Wonders_3_lights.PCX b/Art/Districts/Summer/Annotations/Wonders_3_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Wonders_3_lights.PCX rename to Art/Districts/Summer/Annotations/Wonders_3_lights.PCX diff --git a/Art/Districts/Annotations/Wonders_lights.PCX b/Art/Districts/Summer/Annotations/Wonders_lights.PCX similarity index 100% rename from Art/Districts/Annotations/Wonders_lights.PCX rename to Art/Districts/Summer/Annotations/Wonders_lights.PCX diff --git a/Art/Districts/Winter/.gitignore b/Art/Districts/Winter/.gitignore new file mode 100644 index 00000000..37ab4d22 --- /dev/null +++ b/Art/Districts/Winter/.gitignore @@ -0,0 +1,24 @@ +0100/ +0200/ +0300/ +0400/ +0500/ +0600/ +0700/ +0800/ +0900/ +1000/ +1100/ +1200/ +1300/ +1400/ +1500/ +1600/ +1700/ +1800/ +1900/ +2000/ +2100/ +2200/ +2300/ +2400/ \ No newline at end of file diff --git a/Art/Districts/Winter/Annotations/.gitignore b/Art/Districts/Winter/Annotations/.gitignore new file mode 100644 index 00000000..3a7f50b6 --- /dev/null +++ b/Art/Districts/Winter/Annotations/.gitignore @@ -0,0 +1 @@ +*_lights.pcx \ No newline at end of file diff --git a/C3X.h b/C3X.h index 0c687357..335a8b64 100644 --- a/C3X.h +++ b/C3X.h @@ -173,6 +173,25 @@ enum day_night_cycle_mode { DNCM_SPECIFIED }; +enum seasonal_cycle_mode { + SCM_OFF = 0, + SCM_TIMER, + SCM_USER_SEASON, + SCM_EVERY_TURN, + SCM_ON_DAY_NIGHT_HOUR, + SCM_SPECIFIED +}; + +enum cycle_season { + CS_SUMMER = 0, + CS_FALL, + CS_WINTER, + CS_SPRING, + COUNT_CYCLE_SEASONS +}; + +char const * const cycle_season_names[COUNT_CYCLE_SEASONS] = {"Summer", "Fall", "Winter", "Spring"}; + enum distribution_hub_yield_division_mode { DHYDM_FLAT = 0, DHYDM_SCALE_BY_CITY_COUNT @@ -296,6 +315,7 @@ struct c3x_config { bool show_territory_colors_on_water_tiles_in_minimap; bool convert_some_popups_into_online_mp_messages; bool enable_debug_mode_switch; + bool patch_view_all_tile_animations_in_debug_mode; bool accentuate_cities_on_minimap; enum minimap_doubling_mode double_minimap_size; bool allow_multipage_civilopedia_descriptions; @@ -412,12 +432,21 @@ struct c3x_config { bool enable_named_tiles; + bool enable_custom_animations; char * aircraft_victory_animation; // NULL if set to "none" in config + int show_tile_destruct_animation_after; + int show_tile_destruction_animation_for_turns; int day_night_cycle_mode; int elapsed_minutes_per_day_night_hour_transition; int fixed_hours_per_turn_for_day_night_cycle; int pinned_hour_for_day_night_cycle; + int seasonal_cycle_mode; + int enabled_seasons_mask; + int pinned_season_for_seasonal_cycle; + int elapsed_minutes_per_season_transition; + int fixed_turns_per_season; + int transition_season_on_day_night_hour; bool enable_natural_wonders; bool add_natural_wonders_to_scenarios_if_none; @@ -735,6 +764,21 @@ enum district_ai_build_strategy { DABS_TILE_IMPROVEMENT = 1 }; +struct natural_wonder_animation_config { + char const * ini_path; + unsigned int day_night_hour_mask; // bits 0..23 + unsigned int season_mask; // bits 0..3 + unsigned int culture_group_mask; // bits 0..4 + unsigned int era_mask; // bits 0..3 + enum direction direction; + float frame_time_seconds; + int x_offset; + int y_offset; + bool has_direction; + bool has_frame_time_seconds; + bool has_offsets; +}; + struct district_config { enum Unit_Command_Values command; char const * name; @@ -780,6 +824,8 @@ struct district_config { int buildable_on_district_count; int buildable_adjacent_to_district_count; int img_path_count; + struct natural_wonder_animation_config animations[8]; + int animation_count; int img_column_count; bool has_img_column_count_override; int btn_tile_sheet_column; @@ -908,6 +954,8 @@ struct natural_wonder_district_config { bool impassable; bool impassable_to_wheeled; bool is_dynamic; + struct natural_wonder_animation_config animations[8]; + int animation_count; }; struct natural_wonder_candidate { @@ -921,6 +969,93 @@ struct natural_wonder_candidate_list { int capacity; }; +enum tile_animation_type { + TAT_TERRAIN = 0, + TAT_RESOURCE, + TAT_NATURAL_WONDER, + TAT_DESTRUCT_INITIAL, + TAT_DESTRUCT_AFTER, + TAT_DISTRICT, + TAT_PCX, + TAT_COASTAL_WAVE +}; + +enum tile_destruct_animation_trigger { + TDAT_BOMBARD = 1, + TDAT_PILLAGE = 2, + TDAT_BOMB = 4 +}; + +#define TILE_ANIM_PCX_FILE_UNKNOWN (-1) +enum tile_animation_pcx_file { + TAPF_TERRAINBUILDINGS = 0, + TAPF_WATERFALLS, + TAPF_FLOODPLAINS, + TAPF_DELTARIVERS, + TAPF_MTNRIVERS, + TAPF_IRRIGATION_DESETT, + TAPF_IRRIGATION_PLAINS, + TAPF_IRRIGATION, + TAPF_IRRIGATION_TUNDRA, + TAPF_VOLCANOS, + TAPF_VOLCANOS_FORESTS, + TAPF_VOLCANOS_JUNGLES, + TAPF_VOLCANOS_SNOW, + TAPF_GRASSLAND_FORESTS, + TAPF_PLAINS_FORESTS, + TAPF_TUNDRA_FORESTS, + TAPF_LMFORESTS, + TAPF_MOUNTAINS, + TAPF_MOUNTAIN_FORESTS, + TAPF_MOUNTAIN_JUNGLES, + TAPF_MOUNTAINS_SNOW, + TAPF_XHILLS, + TAPF_HILL_FORESTS, + TAPF_HILL_JUNGLE, + TAPF_LMHILLS, + TAPF_ROADS, + TAPF_RAILROADS +}; + +#define MAX_TILE_ANIMATION_CONFIGS 128 +#define MAX_TILE_ANIMATION_ADJACENCY 8 + +struct tile_animation_adjacent_requirement { + enum SquareTypes square_type; + enum direction direction; + bool has_direction; + bool is_land; +}; + +struct tile_animation_config { + char const * name; + char const * ini_path; + enum tile_animation_type type; + unsigned int terrain_types_mask; + bool terrain_types_include_land; + int resource_id; + int natural_wonder_id; + int district_id; + int pcx_file_id; + int pcx_index; + enum direction direction; + int x_offset; + int y_offset; + float frame_time_seconds; + bool has_direction; + bool has_x_offset; + bool has_y_offset; + bool has_frame_time_seconds; + struct tile_animation_adjacent_requirement adjacent_to[MAX_TILE_ANIMATION_ADJACENCY]; + int adjacent_to_count; + unsigned int day_night_hour_mask; // bits 0..23 + unsigned int season_mask; // bits 0..3 + unsigned int culture_group_mask; // bits 0..4 + unsigned int era_mask; // bits 0..3 + int effect_id; + bool in_use; +}; + struct wonder_location { short x; short y; @@ -1053,6 +1188,8 @@ struct parsed_district_definition { int buildable_on_district_count; int buildable_adjacent_to_district_count; int img_path_count; + struct natural_wonder_animation_config animations[8]; + int animation_count; int img_column_count; bool allow_multiple; bool vary_img_by_era; @@ -1237,8 +1374,10 @@ struct parsed_natural_wonder_definition { int gold_bonus; int shield_bonus; int happiness_bonus; - bool impassable; - bool impassable_to_wheeled; + bool impassable; + bool impassable_to_wheeled; + struct natural_wonder_animation_config animations[8]; + int animation_count; bool has_name; bool has_img_path; bool has_img_row; @@ -1256,6 +1395,44 @@ struct parsed_natural_wonder_definition { bool has_impassable_to_wheeled; }; +struct parsed_tile_animation_definition { + char * name; + char * ini_path; + char * resource_type; + char * pcx_file; + enum tile_animation_type type; + unsigned int terrain_types_mask; + int natural_wonder_id; + int district_id; + int pcx_file_id; + int pcx_index; + bool terrain_types_include_land; + enum direction direction; + int x_offset; + int y_offset; + float frame_time_seconds; + struct tile_animation_adjacent_requirement adjacent_to[MAX_TILE_ANIMATION_ADJACENCY]; + int adjacent_to_count; + unsigned int day_night_hour_mask; + unsigned int season_mask; + unsigned int culture_group_mask; + unsigned int era_mask; + bool has_name; + bool has_ini_path; + bool has_type; + bool has_resource_type; + bool has_pcx_file; + bool has_pcx_index; + bool has_terrain_types; + bool has_direction; + bool has_x_offset; + bool has_y_offset; + bool has_frame_time_seconds; + bool has_adjacent_to; + bool has_day_night_hour_mask; + bool has_season_mask; +}; + struct scenario_district_entry { int tile_x; int tile_y; @@ -1524,6 +1701,7 @@ struct injected_state { } * loaded_config_names; char current_districts_config_path[MAX_PATH]; + char current_tile_animations_config_path[MAX_PATH]; char mod_script_path[MAX_PATH]; @@ -1978,10 +2156,14 @@ struct injected_state { // Day-Night cycle data int current_day_night_cycle; bool day_night_cycle_unstarted; // If current_day_night_cycle has not been set, f.e. because it's the first turn of a new game. + int current_seasonal_cycle; + bool seasonal_cycle_unstarted; + int turns_in_current_season; bool day_night_cycle_img_proxies_indexed; LARGE_INTEGER last_day_night_cycle_update_time; + LARGE_INTEGER last_seasonal_cycle_update_time; - struct table day_night_sprite_proxy_by_hour[24]; + struct table * day_night_sprite_proxy_by_season_and_hour; struct wonder_district_image_set { Sprite img; @@ -2074,7 +2256,7 @@ struct injected_state { Sprite Abandoned_Maritime_District_Image; struct wonder_district_image_set Wonder_District_Images[MAX_WONDER_DISTRICT_TYPES]; struct natural_wonder_district_image_set Natural_Wonder_Images[MAX_NATURAL_WONDER_DISTRICT_TYPES]; - } day_night_cycle_imgs[24]; + } * cycle_imgs; // Districts enum init_state dc_img_state; @@ -2203,6 +2385,34 @@ struct district_button_image_set { // Natural Wonder labels: table mapping natural wonder name strings to their IDs, count of defined natural wonders, struct table natural_wonder_name_to_id; + + // Tile animation definitions and effect ID mapping. + struct tile_animation_config tile_animation_configs[MAX_TILE_ANIMATION_CONFIGS]; + int tile_animation_count; + int tile_animation_effect_base; + int tile_animation_spawn_effect_override; + bool tile_animation_spawn_effect_override_active; + + // Core per-tile selection cache (required for rule matching lookups). + unsigned int * tile_animation_selected_mask_matrix; + int tile_animation_selected_tile_count; + int tile_animation_selected_animation_count; + bool tile_animation_selected_valid; + + // Optional scheduler optimization cache. + byte * tile_animation_selected_next_index; // Cached winner animation index per tile, 0xFF = none. + int * tile_animation_selected_tile_indices; // Tile indices currently having a cached winner. + int tile_animation_selected_match_count; + byte * tile_destruct_animation_ages; // Per tile: 0 = none, 1 = initial, >1 = after. + + // PCX-driven animation rule lookups and active masks. + struct table tile_animation_pcx_sprite_lookup; + struct table tile_animation_pcx_rule_key_to_index; + int tile_animation_pcx_rule_key_count; + unsigned int tile_animation_pcx_rule_masks[MAX_TILE_ANIMATION_CONFIGS][(MAX_TILE_ANIMATION_CONFIGS + 31) / 32]; + unsigned int tile_animation_pcx_word_mask[(MAX_TILE_ANIMATION_CONFIGS + 31) / 32]; + unsigned int tile_animation_pcx_active_word_mask[(MAX_TILE_ANIMATION_CONFIGS + 31) / 32]; + bool tile_animation_has_pcx_rules; struct ai_candidate_bridge_or_canal_entry * ai_candidate_bridge_or_canals; int ai_candidate_bridge_or_canals_count; diff --git a/Civ3Conquests.h b/Civ3Conquests.h index 99e188d1..4964396f 100644 --- a/Civ3Conquests.h +++ b/Civ3Conquests.h @@ -207,7 +207,7 @@ typedef struct Unit_Body Unit_Body; typedef struct Tile Tile; typedef struct Map Map; typedef struct BIC BIC; -typedef struct _1A4 _1A4; +typedef struct Tile_Animated_Effect Tile_Animated_Effect; typedef struct Scroll_Bar Scroll_Bar; typedef struct Base_Form Base_Form; typedef struct Advisor_Military_Form Advisor_Military_Form; @@ -1680,7 +1680,7 @@ struct UnitType int field_CC; int requires_support; int field_D4; - int field_D8; + int active_tile_effect; IntList unit_telepads; int enslave_results_in; IntList stealth_attack_targets; @@ -3913,7 +3913,7 @@ struct Animation_Info Flic_Anim_Info **Animations; int field_140[17]; int field_184[21]; - float *field_1D8; + float *anim_frame_time_seconds; int field_1DC; int field_1E0; int field_1E4; @@ -4391,7 +4391,7 @@ struct Tile_Body short field_92; int field_D0_Visibility; int field_D4; - _1A4 *field_D8; + Tile_Animated_Effect *active_tile_effect; }; struct Race @@ -4490,7 +4490,7 @@ struct City_Body int rally_point_x; int rally_point_y; char CityName[20]; - int field_1D8; + int anim_frame_time_seconds; int Order_Queue_Count; City_Order Orders_Queue[9]; int FoodRequired; @@ -5169,10 +5169,10 @@ struct BIC Map Map; }; -struct _1A4 +struct Tile_Animated_Effect { int V[3]; - FLC_Animation struct_188; + FLC_Animation flc_animation; int field_194[4]; }; @@ -5996,7 +5996,7 @@ struct City_Form int Units_Start_Index; int field_96B0; int field_96B4[5]; - FLC_Animation struct_188; + FLC_Animation flc_animation; City_Form_Labels Labels; int field_98D0[67]; Sprite QueueBase_Image; diff --git a/DayNight/civ3_postprocess_pixels.py b/DayNight/civ3_postprocess_pixels.py index eb8a26c0..c33e19d9 100644 --- a/DayNight/civ3_postprocess_pixels.py +++ b/DayNight/civ3_postprocess_pixels.py @@ -228,6 +228,9 @@ def walk_and_fix(data_dir: str, noon_folder: str, annotations_abs = os.path.normpath(os.path.join(data_dir, "Annotations")) targets = [] + def is_hour_folder_name(name: str) -> bool: + return len(name) == 4 and name.isdigit() + if only_hour is not None: hour_name = f"{only_hour:04d}" hour_abs = os.path.normpath(os.path.join(data_dir, hour_name)) @@ -241,11 +244,15 @@ def walk_and_fix(data_dir: str, noon_folder: str, if not os.path.isdir(sub): continue if os.path.normcase(sub) == os.path.normcase(noon_abs): continue if os.path.normcase(sub) == os.path.normcase(annotations_abs): continue + if not is_hour_folder_name(name): continue targets.append(sub) total_changed = 0 for root in targets: - for dirpath, _, filenames in os.walk(root): + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d.lower() != "annotations"] + if os.path.basename(dirpath).lower() == "annotations": + continue for fname in filenames: if not fname.lower().endswith(".pcx"): continue diff --git a/DayNight/copy_rotate_civ3_pcx_rows.py b/DayNight/copy_rotate_civ3_pcx_rows.py new file mode 100644 index 00000000..6df6ae37 --- /dev/null +++ b/DayNight/copy_rotate_civ3_pcx_rows.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +copy_rotate_civ3_pcx_rows.py + +Copies the first animation row (row 0) of a Civ3-style indexed PCX sheet into +rows 1..3, transforming each frame so the motion direction changes: + +- Row 1: NW -> SW (vertical flip) +- Row 2: NW -> NE (horizontal flip) +- Row 3: NW -> SE (180° rotate) + +Assumes: +- Indexed PCX (mode 'P') +- 64 columns, 8 rows by default +- 1-pixel border grid between slots and around the outer edge +- Background is magenta (#ff00ff), border is (#c000ff) -- used only for sanity checks + +This script preserves the palette and keeps everything indexed. + +Example: + python copy_rotate_civ3_pcx_rows.py --in Waves.pcx --out Waves_dirs.pcx +""" + +import argparse +import sys +from PIL import Image + +MAGENTA = (255, 0, 255) # background +BORDER = (192, 0, 255) # 1px grid/border + + +def find_palette_index(pal_bytes, rgb): + """Return palette index (0..255) matching rgb, or None.""" + r_t, g_t, b_t = rgb + if pal_bytes is None or len(pal_bytes) < 768: + return None + for i in range(256): + r, g, b = pal_bytes[i*3:i*3+3] + if (r, g, b) == (r_t, g_t, b_t): + return i + return None + + +def compute_grid(img_w, img_h, cols, rows, border): + """ + Compute slot rectangles assuming: + total_w = cols*slot_w + (cols+1)*border + total_h = rows*slot_h + (rows+1)*border + Returns: (slot_w, slot_h, x0_list, y0_list) + where x0_list[c] is left edge of slot interior (excluding border), + and y0_list[r] is top edge of slot interior. + """ + slot_w = (img_w - (cols + 1) * border) // cols + slot_h = (img_h - (rows + 1) * border) // rows + + expected_w = cols * slot_w + (cols + 1) * border + expected_h = rows * slot_h + (rows + 1) * border + if expected_w != img_w or expected_h != img_h: + raise ValueError( + f"Image size {img_w}x{img_h} is not consistent with cols={cols}, rows={rows}, border={border}.\n" + f"Computed slot {slot_w}x{slot_h} gives expected {expected_w}x{expected_h}." + ) + + x0_list = [border + c * (slot_w + border) for c in range(cols)] + y0_list = [border + r * (slot_h + border) for r in range(rows)] + return slot_w, slot_h, x0_list, y0_list + + +def crop_slot(img, x0, y0, slot_w, slot_h): + return img.crop((x0, y0, x0 + slot_w, y0 + slot_h)) + + +def paste_slot(dst, src_slot, x0, y0): + # Direct paste keeps indices unchanged (mode 'P') + dst.paste(src_slot, (x0, y0)) + + +def fill_slot(dst, x0, y0, slot_w, slot_h, fill_index): + patch = Image.new("P", (slot_w, slot_h), color=fill_index) + patch.putpalette(dst.getpalette()) + dst.paste(patch, (x0, y0)) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--in", dest="in_path", required=True, help="Input indexed PCX sheet") + ap.add_argument("--out", dest="out_path", required=True, help="Output indexed PCX sheet") + ap.add_argument("--cols", type=int, default=64, help="Number of columns (default 64)") + ap.add_argument("--rows", type=int, default=8, help="Number of rows (default 8)") + ap.add_argument("--border", type=int, default=1, help="Border thickness in pixels (default 1)") + ap.add_argument("--src_row", type=int, default=0, help="Source row index to copy from (default 0)") + ap.add_argument("--dst_rows", default="1,2,3", + help="Comma-separated destination row indices (default '1,2,3')") + ap.add_argument("--no_clear", action="store_true", + help="Do not clear destination slots to magenta before pasting") + args = ap.parse_args() + + # Load + im = Image.open(args.in_path) + if im.mode != "P": + raise ValueError(f"Input must be indexed (mode 'P'). Got mode={im.mode}") + + pal = im.getpalette() + pal_bytes = bytes(pal) if pal is not None else None + bg_idx = find_palette_index(pal_bytes, MAGENTA) + border_idx = find_palette_index(pal_bytes, BORDER) + + if bg_idx is None: + print("WARNING: Could not find magenta (#ff00ff) in palette. Clearing (if enabled) may be wrong.", file=sys.stderr) + if border_idx is None: + print("WARNING: Could not find border color (#c000ff) in palette. That's OK; it's only used for sanity.", file=sys.stderr) + + cols = args.cols + rows = args.rows + border = args.border + + slot_w, slot_h, x0_list, y0_list = compute_grid(im.width, im.height, cols, rows, border) + + # Parse destination rows + dst_rows = [] + for part in args.dst_rows.split(","): + part = part.strip() + if not part: + continue + dst_rows.append(int(part)) + + if args.src_row < 0 or args.src_row >= rows: + raise ValueError(f"src_row {args.src_row} out of range 0..{rows-1}") + for r in dst_rows: + if r < 0 or r >= rows: + raise ValueError(f"dst_row {r} out of range 0..{rows-1}") + + out = im.copy() + + # Define transforms relative to NW source + # Row 1: SW = vertical flip + # Row 2: NE = horizontal flip + # Row 3: SE = 180 rotate + # If user specifies different dst_rows ordering, we map by row number. + transform_by_row = { + 1: ("SW", lambda s: s.transpose(Image.FLIP_TOP_BOTTOM)), + 2: ("NE", lambda s: s.transpose(Image.FLIP_LEFT_RIGHT)), + 3: ("SE", lambda s: s.transpose(Image.ROTATE_180)), + } + + # Copy each column slot from src_row to each dst_row with transform + for c in range(cols): + sx0 = x0_list[c] + sy0 = y0_list[args.src_row] + src_slot = crop_slot(im, sx0, sy0, slot_w, slot_h) + + for r in dst_rows: + if r == args.src_row: + continue + + label, xf = transform_by_row.get(r, (f"row{r}", lambda s: s)) + dx0 = x0_list[c] + dy0 = y0_list[r] + + if (not args.no_clear) and (bg_idx is not None): + fill_slot(out, dx0, dy0, slot_w, slot_h, bg_idx) + + dst_slot = xf(src_slot) + + # Safety: ensure size matches (it should with flips/180) + if dst_slot.size != (slot_w, slot_h): + raise ValueError( + f"Transformed slot size {dst_slot.size} does not match slot {slot_w}x{slot_h} " + f"(dst row {r} {label}). If you tried to use 90° rotation, it won't fit unless slots are square." + ) + + paste_slot(out, dst_slot, dx0, dy0) + + # Ensure palette preserved + out.putpalette(im.getpalette()) + + # Save as PCX + out.save(args.out_path, format="PCX") + print(f"Done. Wrote: {args.out_path}") + print(f"Detected slot size: {slot_w}x{slot_h}, border={border}, cols={cols}, rows={rows}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/DayNight/flc_night_darkener.py b/DayNight/flc_night_darkener.py new file mode 100644 index 00000000..42e2ce15 --- /dev/null +++ b/DayNight/flc_night_darkener.py @@ -0,0 +1,810 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Darken a Civ3 FLC animation using the same palette tonemapping math as civ3_day_night.py. + +Key behavior: +- Reads an input FLC and decodes animation frames (supports COLOR_256, BYTE_RUN, FLI_COPY, + DELTA_FLC, DELTA_FLI, BLACK). +- Computes which palette indices are actually used by animation frames. +- Applies civ3_day_night palette darkening to only those used indices. +- Preserves reserved Civ3 indices/colors (by default first 63 and last 20 indices, plus #FF00FF). +- Re-emits a Civ3-compatible FLC (BYTE_RUN keyframe + COLOR_256 + LITERAL animation frames). +""" + +import argparse +import hashlib +import struct +from dataclasses import dataclass +from typing import Iterable, List, Sequence, Set, Tuple + +import civ3_day_night as dn + +FLC_MAGIC = 0xAF12 +CHUNK_FRAME = 0xF1FA +CHUNK_COLOR_256 = 4 +CHUNK_DELTA_FLC = 7 +CHUNK_DELTA_FLI = 12 +CHUNK_BLACK = 13 +CHUNK_BYTE_RUN = 15 +CHUNK_FLI_COPY = 16 + +CIV3_CREATOR_DEFAULT = 0xF1F1F2F2 + + +def u16(data: bytes, off: int) -> int: + return struct.unpack_from(" int: + return struct.unpack_from(" int: + return struct.unpack_from(" int: + return b - 256 if b > 127 else b + + +def pack_u16(v: int) -> bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + core = ( + pack_u32(28) + + pack_i32(0) + + pack_u16(self.num_anims) + + pack_u16(self.anim_length) + + pack_u16(self.x_offset) + + pack_u16(self.y_offset) + + pack_u16(self.xs_orig) + + pack_u16(self.ys_orig) + + pack_u32(self.anim_time_ms) + + pack_i32(self.directions) + ) + return core + (b"\x00" * 12) + + +@dataclass +class FlcDecoded: + w: int + h: int + file_frames: int + speed_ms: int + creator: int + civ3: Civ3Tail + palette: List[int] + anim_frames: List[bytes] + include_ring_frame: bool + direction_uniques: List[int] + + +def parse_civ3_tail(hdr: bytes, w: int, h: int, frame_count: int) -> Civ3Tail: + if len(hdr) < 128: + return Civ3Tail(1, frame_count, 0, 0, w, h, frame_count * 100, 1) + try: + num_anims = u16(hdr, 96) + anim_length = u16(hdr, 98) + x_offset = u16(hdr, 100) + y_offset = u16(hdr, 102) + xs_orig = u16(hdr, 104) + ys_orig = u16(hdr, 106) + anim_time_ms = u32(hdr, 108) + directions = struct.unpack_from(" None: + if len(payload) < 2: + return + packets = u16(payload, 0) + p = 2 + idx = 0 + for _ in range(packets): + if p + 2 > len(payload): + break + skip = payload[p] + count = payload[p + 1] + p += 2 + idx += skip + if count == 0: + count = 256 + for _ in range(count): + if p + 3 > len(payload) or idx >= 256: + break + pal[3 * idx + 0] = payload[p + 0] + pal[3 * idx + 1] = payload[p + 1] + pal[3 * idx + 2] = payload[p + 2] + p += 3 + idx += 1 + + +def decode_byte_run(payload: bytes, w: int, h: int) -> bytes: + out = bytearray(w * h) + p = 0 + for y in range(h): + if p >= len(payload): + break + p += 1 + x = 0 + row_off = y * w + while x < w and p < len(payload): + n = s8(payload[p]) + p += 1 + if n >= 0: + if p >= len(payload): + break + b = payload[p] + p += 1 + run = min(n, w - x) + out[row_off + x:row_off + x + run] = bytes([b]) * run + x += run + else: + run = min(-n, w - x, len(payload) - p) + out[row_off + x:row_off + x + run] = payload[p:p + run] + p += run + x += run + return bytes(out) + + +def decode_delta_fli(payload: bytes, frame: bytearray, w: int, h: int) -> None: + if len(payload) < 4: + return + y = u16(payload, 0) + lines = u16(payload, 2) + p = 4 + while lines > 0 and y < h and p < len(payload): + packets = payload[p] + p += 1 + x = 0 + row_off = y * w + for _ in range(packets): + if p + 2 > len(payload): + break + x += payload[p] + p += 1 + n = s8(payload[p]) + p += 1 + if n >= 0: + cnt = n + avail = min(cnt, len(payload) - p) + write = min(avail, max(0, w - x)) + if write > 0: + frame[row_off + x:row_off + x + write] = payload[p:p + write] + x += write + p += avail + x += max(0, cnt - write) + else: + if p >= len(payload): + break + b = payload[p] + p += 1 + run = min(-n, w - x) + frame[row_off + x:row_off + x + run] = bytes([b]) * run + x += run + y += 1 + lines -= 1 + + +def decode_delta_flc(payload: bytes, frame: bytearray, w: int, h: int) -> None: + if len(payload) < 2: + return + lines = u16(payload, 0) + p = 2 + y = 0 + while lines > 0 and y < h and p + 2 <= len(payload): + op = s16(payload, p) + p += 2 + + if op < 0: + flag = op & 0xC000 + if flag == 0xC000: + y += -op + continue + if flag == 0x8000: + if w > 0 and y < h: + frame[y * w + (w - 1)] = op & 0xFF + continue + continue + + packets = op + x = 0 + row_off = y * w + for _ in range(packets): + if p + 2 > len(payload): + break + x += payload[p] + p += 1 + n = s8(payload[p]) + p += 1 + if n >= 0: + cnt = n * 2 + avail = min(cnt, len(payload) - p) + write = min(avail, max(0, w - x)) + if write > 0: + frame[row_off + x:row_off + x + write] = payload[p:p + write] + x += write + p += avail + x += max(0, cnt - write) + else: + if p + 2 > len(payload): + break + b0 = payload[p] + b1 = payload[p + 1] + p += 2 + reps = -n + for _ in range(reps): + if x + 1 >= w: + break + frame[row_off + x] = b0 + frame[row_off + x + 1] = b1 + x += 2 + + y += 1 + lines -= 1 + + +def decode_flc(path: str) -> FlcDecoded: + data = open(path, "rb").read() + if len(data) < 128: + raise SystemExit("Input file is too small to be a valid FLC.") + magic = u16(data, 4) + if magic != FLC_MAGIC: + raise SystemExit(f"Unsupported magic 0x{magic:04X}; expected 0x{FLC_MAGIC:04X}.") + + file_frames = u16(data, 6) + w = u16(data, 8) + h = u16(data, 10) + speed_ms = u32(data, 16) + creator = u32(data, 26) + + pal = [0] * (256 * 3) + frame = bytearray(w * h) + + decoded_all: List[bytes] = [] + frame_is_brun: List[bool] = [] + off = 128 + while off + 6 <= len(data): + csize = u32(data, off) + ctype = u16(data, off + 4) + if csize < 6 or off + csize > len(data): + break + if ctype == CHUNK_FRAME and csize >= 16: + nsub = u16(data, off + 6) + sub_off = off + 16 + frame_end = off + csize + has_brun = False + for _ in range(nsub): + if sub_off + 6 > frame_end: + break + ssize = u32(data, sub_off) + stype = u16(data, sub_off + 4) + if ssize < 6 or sub_off + ssize > frame_end: + break + payload = data[sub_off + 6:sub_off + ssize] + + if stype == CHUNK_COLOR_256: + decode_color_256(payload, pal) + elif stype == CHUNK_BYTE_RUN: + has_brun = True + frame = bytearray(decode_byte_run(payload, w, h)) + elif stype == CHUNK_FLI_COPY: + need = w * h + if len(payload) >= need: + frame = bytearray(payload[:need]) + elif stype == CHUNK_BLACK: + frame = bytearray(w * h) + elif stype == CHUNK_DELTA_FLI: + decode_delta_fli(payload, frame, w, h) + elif stype == CHUNK_DELTA_FLC: + decode_delta_flc(payload, frame, w, h) + sub_off += ssize + + decoded_all.append(bytes(frame)) + frame_is_brun.append(has_brun) + off += csize + + civ3 = parse_civ3_tail(data[:128], w, h, max(1, file_frames)) + expected_anim = max(1, int(file_frames)) + + # Prefer Civ3 multi-direction layout: per direction => keyframe + anim_length frames. + extracted = False + anim_frames: List[bytes] + include_ring = False + if civ3.num_anims > 0 and civ3.anim_length > 0 and civ3.num_anims * civ3.anim_length == expected_anim: + per_dir = civ3.anim_length + dirs = civ3.num_anims + needed = dirs * (per_dir + 1) + if len(decoded_all) >= needed: + temp: List[bytes] = [] + ok = True + for d in range(dirs): + k = d * (per_dir + 1) + if not frame_is_brun[k]: + ok = False + break + a0 = k + 1 + a1 = a0 + per_dir + if a1 > len(decoded_all): + ok = False + break + temp.extend(decoded_all[a0:a1]) + if ok and len(temp) == expected_anim: + anim_frames = temp + include_ring = len(decoded_all) > needed + extracted = True + + if not extracted and len(decoded_all) >= expected_anim + 1: + # Single keyframe layout: one leading BYTE_RUN keyframe, then animated frames. + anim_frames = decoded_all[1:1 + expected_anim] + include_ring = len(decoded_all) > (1 + expected_anim) + elif len(decoded_all) >= expected_anim: + anim_frames = decoded_all[:expected_anim] + include_ring = len(decoded_all) > expected_anim + else: + anim_frames = decoded_all[:] if decoded_all else [bytes(frame)] + include_ring = False + + if civ3.num_anims <= 0: + civ3.num_anims = 1 + if expected_anim % civ3.num_anims == 0: + civ3.anim_length = expected_anim // civ3.num_anims + else: + civ3.num_anims = 1 + civ3.anim_length = expected_anim + direction_uniques: List[int] = [] + if civ3.num_anims > 0 and len(anim_frames) >= civ3.num_anims and len(anim_frames) % civ3.num_anims == 0: + per_dir = len(anim_frames) // civ3.num_anims + for d in range(civ3.num_anims): + seg = anim_frames[d * per_dir:(d + 1) * per_dir] + direction_uniques.append(len(set(seg))) + + return FlcDecoded( + w=w, + h=h, + file_frames=expected_anim, + speed_ms=speed_ms, + creator=creator if creator else CIV3_CREATOR_DEFAULT, + civ3=civ3, + palette=pal, + anim_frames=anim_frames, + include_ring_frame=include_ring, + direction_uniques=direction_uniques, + ) + + +def iter_color_chunk_payload_spans(data: bytes) -> List[Tuple[int, int]]: + spans: List[Tuple[int, int]] = [] + off = 128 + n = len(data) + while off + 6 <= n: + csize = u32(data, off) + ctype = u16(data, off + 4) + if csize < 6 or off + csize > n: + break + if ctype == CHUNK_FRAME and csize >= 16: + frame_end = off + csize + nsub = u16(data, off + 6) + so = off + 16 + for _ in range(nsub): + if so + 6 > frame_end: + break + ssize = u32(data, so) + stype = u16(data, so + 4) + if ssize < 6 or so + ssize > frame_end: + break + if stype == CHUNK_COLOR_256: + spans.append((so + 6, so + ssize)) + so += ssize + off += csize + return spans + + +def patch_color_256_payload(payload: bytearray, new_pal: Sequence[int]) -> None: + if len(payload) < 2: + return + packets = u16(payload, 0) + p = 2 + idx = 0 + for _ in range(packets): + if p + 2 > len(payload): + break + skip = payload[p] + count = payload[p + 1] + p += 2 + idx += skip + cnt = 256 if count == 0 else count + for _ in range(cnt): + if p + 3 > len(payload) or idx >= 256: + break + payload[p + 0] = new_pal[3 * idx + 0] + payload[p + 1] = new_pal[3 * idx + 1] + payload[p + 2] = new_pal[3 * idx + 2] + p += 3 + idx += 1 + + +def patch_flc_palette_only(inp: str, out: str, new_pal: Sequence[int]) -> int: + data = bytearray(open(inp, "rb").read()) + spans = iter_color_chunk_payload_spans(data) + for a, b in spans: + payload = bytearray(data[a:b]) + patch_color_256_payload(payload, new_pal) + data[a:b] = payload + with open(out, "wb") as f: + f.write(data) + return len(spans) + + +def parse_ranges(spec: str) -> Set[int]: + out: Set[int] = set() + for raw in (spec or "").split(","): + s = raw.strip() + if not s: + continue + if "-" in s: + a_str, b_str = s.split("-", 1) + a = int(a_str) + b = int(b_str) + if a > b: + a, b = b, a + for i in range(max(0, a), min(255, b) + 1): + out.add(i) + else: + i = int(s) + if 0 <= i <= 255: + out.add(i) + return out + + +def parse_rgb(s: str) -> Tuple[int, int, int]: + return dn.parse_rgb(s) + + +def is_magenta_like(rgb: Tuple[int, int, int]) -> bool: + return rgb[0] == 255 and rgb[2] == 255 + + +def used_indices(frames: Sequence[bytes]) -> Set[int]: + used: Set[int] = set() + for fr in frames: + used.update(fr) + return used + + +def apply_darkening_to_used_indices( + pal: List[int], + used: Set[int], + reserved_indices: Set[int], + reserved_colors: Set[Tuple[int, int, int]], + preserve_magenta_like: bool, + *, + hour: float, + warmth: float, + blue: float, + darkness: float, + desat: float, + sat: float, + contrast: float, + sunrise_center: float, + sunset_center: float, + twilight_width: float, + noon_blend: float, + noon_sigma: float, + noon_window_start: float, + noon_window_end: float, + noon_window_soft: float, +) -> Tuple[List[int], int]: + adjusted = dn.adjust_palette_for_time( + pal, + hour, + reserved_colors, + reserved_indices=reserved_indices, + warmth_scale=warmth, + blue_scale=blue, + darkness_scale=darkness, + desat_scale=desat, + sat_boost=sat, + contrast=contrast, + sunrise_center=sunrise_center, + sunset_center=sunset_center, + twilight_sigma=twilight_width, + noon_blend=noon_blend, + noon_sigma=noon_sigma, + noon_window_start=noon_window_start, + noon_window_end=noon_window_end, + noon_window_soft=noon_window_soft, + ) + + out = pal[:] + changed = 0 + for i in sorted(used): + if i in reserved_indices: + continue + r, g, b = pal[3 * i:3 * i + 3] + if (r, g, b) in reserved_colors: + continue + if preserve_magenta_like and is_magenta_like((r, g, b)): + continue + + nr, ng, nb = adjusted[3 * i:3 * i + 3] + if (nr, ng, nb) != (r, g, b): + changed += 1 + out[3 * i + 0] = nr + out[3 * i + 1] = ng + out[3 * i + 2] = nb + return out, changed + + +def make_color_256_payload(pal768: bytes) -> bytes: + return struct.pack(" bytes: + size = 6 + len(payload) + return pack_u32(size) + pack_u16(chunk_type) + payload + + +def make_frame_chunk(subchunks: List[bytes]) -> bytes: + payload = b"".join(subchunks) + size = 16 + len(payload) + return pack_u32(size) + pack_u16(CHUNK_FRAME) + pack_u16(len(subchunks)) + (b"\x00" * 8) + payload + + +def make_byte_run_payload(pix: bytes, w: int, h: int) -> bytes: + out = bytearray() + for y in range(h): + row = pix[y * w:(y + 1) * w] + # Civ3FlcEdit writes this as 0xCD and ignores it on read. + out.append(0xCD) + x = 0 + while x < w: + run = 1 + while x + run < w and run < 127 and row[x + run] == row[x]: + run += 1 + if run >= 2: + out.append(run & 0xFF) + out.append(row[x]) + x += run + continue + + start = x + x += 1 + while x < w and (x - start) < 127: + look = 1 + while x + look < w and look < 127 and row[x + look] == row[x]: + look += 1 + if look >= 2: + break + x += 1 + n = x - start + out.append((256 - n) & 0xFF) + out.extend(row[start:x]) + return bytes(out) + + +def build_flc_header_128( + total_file_size: int, + frames_without_ring: int, + w: int, + h: int, + speed_ms: int, + creator: int, + oframe1: int, + oframe2: int, + civ3_tail_40: bytes, +) -> bytes: + hdr = bytearray(b"\x00" * 128) + hdr[0:4] = pack_u32(total_file_size) + hdr[4:6] = pack_u16(FLC_MAGIC) + hdr[6:8] = pack_u16(frames_without_ring) + hdr[8:10] = pack_u16(w) + hdr[10:12] = pack_u16(h) + hdr[12:14] = pack_u16(8) + hdr[14:16] = pack_u16(0x0003) + hdr[16:20] = pack_u32(speed_ms) + hdr[26:30] = pack_u32(creator) + hdr[38:40] = pack_u16(1) + hdr[40:42] = pack_u16(1) + hdr[80:84] = pack_u32(oframe1) + hdr[84:88] = pack_u32(oframe2) + hdr[88:128] = civ3_tail_40 + return bytes(hdr) + + +def write_flc(path: str, decoded: FlcDecoded, palette: Sequence[int]) -> None: + if len(palette) != 768: + raise ValueError("Palette must be 768 bytes.") + w, h = decoded.w, decoded.h + frames = decoded.anim_frames + if not frames: + raise SystemExit("No animation frames decoded from input FLC.") + + civ = decoded.civ3 + target_frames = max(1, decoded.file_frames) + if len(frames) > target_frames: + frames = frames[:target_frames] + elif len(frames) < target_frames: + raise SystemExit( + f"Decoded only {len(frames)} animation frames, but header expects {target_frames}. " + "Refusing to pad/repeat frames." + ) + + if civ.num_anims <= 0: + civ.num_anims = 1 + if target_frames % civ.num_anims == 0: + civ.anim_length = target_frames // civ.num_anims + else: + civ.num_anims = 1 + civ.anim_length = target_frames + civ_tail = civ.pack_40_bytes() + + chunks: List[bytes] = [] + anim_len = max(1, civ.anim_length) + dir_count = max(1, civ.num_anims) + + if dir_count * anim_len != len(frames): + dir_count = 1 + anim_len = len(frames) + civ.num_anims = 1 + civ.anim_length = anim_len + civ_tail = civ.pack_40_bytes() + + for d in range(dir_count): + base = d * anim_len + key_fr = frames[base] + if d == 0: + key_sub = [ + make_subchunk(CHUNK_BYTE_RUN, make_byte_run_payload(key_fr, w, h)), + make_subchunk(CHUNK_COLOR_256, make_color_256_payload(bytes(palette))), + ] + else: + key_sub = [make_subchunk(CHUNK_BYTE_RUN, make_byte_run_payload(key_fr, w, h))] + chunks.append(make_frame_chunk(key_sub)) + + for i in range(anim_len): + fr = frames[base + i] + chunks.append(make_frame_chunk([make_subchunk(CHUNK_FLI_COPY, fr)])) + + if decoded.include_ring_frame: + chunks.append(make_frame_chunk([make_subchunk(CHUNK_BYTE_RUN, make_byte_run_payload(frames[0], w, h))])) + + body = b"".join(chunks) + total = 128 + len(body) + frame_count = target_frames + + header = build_flc_header_128( + total_file_size=total, + frames_without_ring=frame_count, + w=w, + h=h, + speed_ms=decoded.speed_ms, + creator=decoded.creator, + oframe1=128, + oframe2=0, + civ3_tail_40=civ_tail, + ) + + with open(path, "wb") as f: + f.write(header) + f.write(body) + + +def main() -> None: + p = argparse.ArgumentParser( + description="Darken a Civ3 FLC palette for night mode using civ3_day_night tonemapping." + ) + p.add_argument("--in", dest="inp", required=True, help="Input FLC path.") + p.add_argument("--out", required=True, help="Output FLC path.") + + p.add_argument("--hour", type=float, default=22.0, + help="Target hour used for tonemapping (default 22.0 = ~10pm).") + + p.add_argument("--warmth", type=float, default=1.10) + p.add_argument("--blue", type=float, default=1.12) + p.add_argument("--darkness", type=float, default=1.08) + p.add_argument("--desat", type=float, default=0.85) + p.add_argument("--sat", type=float, default=1.05) + p.add_argument("--contrast", type=float, default=1.03) + p.add_argument("--sunrise-center", type=float, default=6.0) + p.add_argument("--sunset-center", type=float, default=18.0) + p.add_argument("--twilight-width", type=float, default=1.8) + + p.add_argument("--noon-blend", type=float, default=0.85) + p.add_argument("--noon-sigma", type=float, default=1.1) + p.add_argument("--noon-window-start", type=float, default=10.0) + p.add_argument("--noon-window-end", type=float, default=14.0) + p.add_argument("--noon-window-soft", type=float, default=0.7) + + p.add_argument("--reserve-index-ranges", default="0-62,236-255", + help="Comma-separated index ranges to preserve (default first 63 and last 20).") + p.add_argument("--preserve-rgb", action="append", default=["#ff00ff"], + help="Exact RGB colors to preserve. Repeatable. Default includes #ff00ff.") + p.add_argument("--no-preserve-magenta-like", action="store_true", + help="Allow changing colors with R=255 and B=255 (disabled by default).") + + args = p.parse_args() + + dec = decode_flc(args.inp) + used = used_indices(dec.anim_frames) + + reserved_indices = parse_ranges(args.reserve_index_ranges) + reserved_colors = set(parse_rgb(s) for s in args.preserve_rgb) + + new_pal, changed = apply_darkening_to_used_indices( + dec.palette, + used=used, + reserved_indices=reserved_indices, + reserved_colors=reserved_colors, + preserve_magenta_like=not args.no_preserve_magenta_like, + hour=args.hour, + warmth=args.warmth, + blue=args.blue, + darkness=args.darkness, + desat=args.desat, + sat=args.sat, + contrast=args.contrast, + sunrise_center=args.sunrise_center, + sunset_center=args.sunset_center, + twilight_width=args.twilight_width, + noon_blend=args.noon_blend, + noon_sigma=args.noon_sigma, + noon_window_start=args.noon_window_start, + noon_window_end=args.noon_window_end, + noon_window_soft=args.noon_window_soft, + ) + + patched_chunks = patch_flc_palette_only(args.inp, args.out, new_pal) + frame_hashes = [hashlib.md5(fr).hexdigest() for fr in dec.anim_frames] + unique_frames = len(set(frame_hashes)) + print(f"Input header frames: {dec.file_frames}") + print(f"Output animation frames: {dec.file_frames}") + print(f"Unique decoded animation frames: {unique_frames}") + if dec.direction_uniques: + print(f"Per-direction unique decoded frames: {dec.direction_uniques}") + print(f"Patched COLOR_256 chunks: {patched_chunks}") + print(f"Used palette indices in animation: {len(used)}") + print(f"Changed used indices: {changed}") + print(f"Wrote: {args.out}") + + +if __name__ == "__main__": + main() diff --git a/DayNight/generate.sh b/DayNight/generate.sh index 15e9c8f0..044f9cbc 100644 --- a/DayNight/generate.sh +++ b/DayNight/generate.sh @@ -5,16 +5,12 @@ set -euo pipefail ### === CONFIG === # Assumed to be run from DayNight/ directory -DAYNIGHT_ANNOTATION_DIR="../Art/DayNight/Annotations" DAYNIGHT_DATA_DIR="../Art/DayNight" -DISTRICT_ANNOTATION_DIR="../Art/Districts/Annotations" DISTRICT_DATA_DIR="../Art/Districts" -ANNOTATION_DIR="../Art/DayNight/Annotations" -DATA_DIR="../Art/DayNight" - NOON_SUBFOLDER="1200" ONLY_HOUR="" # set empty "" to process all hours +BASE_SEASON="Summer" # ---- Day/Night settings ---- WARMTH=1.7 # Scale for sunrise/sunset warmth (1.0 = base) @@ -204,5 +200,93 @@ process_art_set() { fi } -process_art_set "$DAYNIGHT_DATA_DIR" "$DAYNIGHT_ANNOTATION_DIR" -process_art_set "$DISTRICT_DATA_DIR" "$DISTRICT_ANNOTATION_DIR" +ensure_hour_folders() { + local season_dir="$1" + local hour + local hour_folder + for hour in {1..24}; do + printf -v hour_folder "%02d00" "$hour" + mkdir -p "$season_dir/$hour_folder" + done +} + +ensure_season_noon_folder() { + local season_dir="$1" + local summer_noon_dir="$2" + local season_noon_dir="$season_dir/$NOON_SUBFOLDER" + + if [[ -d "$season_noon_dir" ]]; then + return + fi + + if [[ ! -d "$summer_noon_dir" ]]; then + echo "Missing $NOON_SUBFOLDER and no summer source found for $season_dir" >&2 + return + fi + + cp -R "$summer_noon_dir" "$season_noon_dir" + echo "Copied missing $NOON_SUBFOLDER for $season_dir from $summer_noon_dir" +} + +ensure_season_annotations() { + local season_dir="$1" + local summer_annotation_dir="$2" + local season_annotation_dir="$season_dir/Annotations" + local copied_count=0 + local src + local target + + if [[ ! -d "$season_annotation_dir" ]]; then + mkdir -p "$season_annotation_dir" + fi + + if [[ ! -d "$summer_annotation_dir" ]]; then + echo "Missing annotations and no summer source found for $season_dir" >&2 + return + fi + + shopt -s nullglob + shopt -s nocaseglob + for src in "$summer_annotation_dir"/*_lights.pcx; do + target="$season_annotation_dir/$(basename "$src")" + if [[ ! -e "$target" ]]; then + cp "$src" "$target" + copied_count=$((copied_count + 1)) + fi + done + shopt -u nocaseglob + shopt -u nullglob + + if (( copied_count > 0 )); then + echo "Filled $copied_count missing annotations in $season_annotation_dir from $summer_annotation_dir" + fi +} + +process_art_root() { + local art_root="$1" + local summer_annotation_dir="$art_root/$BASE_SEASON/Annotations" + local summer_noon_dir="$art_root/$BASE_SEASON/$NOON_SUBFOLDER" + local season_dir + local season_name + + if [[ ! -d "$art_root" ]]; then + echo "Skipping missing art root: $art_root" >&2 + return + fi + + while IFS= read -r season_dir; do + season_name="$(basename "$season_dir")" + + if [[ "$season_name" == "Annotations" ]] || [[ "$season_name" =~ ^[0-9]{4}$ ]]; then + continue + fi + + ensure_season_noon_folder "$season_dir" "$summer_noon_dir" + ensure_hour_folders "$season_dir" + ensure_season_annotations "$season_dir" "$summer_annotation_dir" + process_art_set "$season_dir" "$season_dir/Annotations" + done < <(find "$art_root" -mindepth 1 -maxdepth 1 -type d | sort) +} + +process_art_root "$DAYNIGHT_DATA_DIR" +process_art_root "$DISTRICT_DATA_DIR" diff --git a/DayNight/mp4_to_civ3_pcx.py b/DayNight/mp4_to_civ3_pcx.py new file mode 100644 index 00000000..a6ca2f90 --- /dev/null +++ b/DayNight/mp4_to_civ3_pcx.py @@ -0,0 +1,768 @@ +#!/usr/bin/env python3 +""" +mp4_to_civ3_sheet.py + +Build a Civ3-style sheet from an MP4: +- N columns sampled by time OR by frame index +- 8 rows (only first row contains frames) +- Other rows magenta fill +- SINGLE shared 1px green grid lines (no doubled borders) + +Output modes: +- indexed: Civ3-style indexed palette and PCX output +- rgb: RGB output (PCX supported; PNG also fine) + +Optional cutout (Option B: MOG2 background subtraction): +- Learn a background model across the sampled frames +- Foreground mask per frame +- Cleanup + keep only largest blob +- Everything else magenta + +Key knobs added: +- --sample_by frame/time, with backoff handling +- --fit crop/contain (contain avoids chopping fish at edges by letterboxing/padding) +- --zoom (scales the frame up/down before fit; helps make subject larger in-slot) + +Dependencies: + pip install pillow opencv-python numpy +""" + +from __future__ import annotations + +import argparse +import math +import os +from typing import List, Tuple + +import cv2 +import numpy as np +from PIL import Image + +MAGENTA = (255, 0, 255) +GREEN = (0, 255, 0) +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) + +RESERVED_CIV_COLORS = 64 +RESERVED_TAIL = 2 # green + magenta +TOTAL_PALETTE = 256 +SAMPLED_COLORS = TOTAL_PALETTE - RESERVED_CIV_COLORS - RESERVED_TAIL # 190 + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser() + p.add_argument("--mp4", required=True, help="Path to input mp4") + p.add_argument("--columns", type=int, required=True, help="Number of columns (sampled frames)") + p.add_argument("--slot_w", type=int, required=True, help="Slot inner width (excluding border)") + p.add_argument("--slot_h", type=int, required=True, help="Slot inner height (excluding border)") + p.add_argument( + "--start_time", + type=float, + default=0.0, + help="Start time in seconds for extraction window (default: 0.0).", + ) + p.add_argument( + "--end_time", + type=float, + default=None, + help="End time in seconds for extraction window (default: end of video).", + ) + p.add_argument( + "--out", + required=True, + help="Output file path (.pcx recommended for your workflow; png also supported). Must be a file, not a directory.", + ) + + # Fit strategy (fixes “fish bottom cut off”) + p.add_argument( + "--fit", + choices=["crop", "contain"], + default="contain", + help="How to adapt frames to slot aspect. crop=center-crop to fill; contain=scale to fit and pad (prevents cutoff).", + ) + p.add_argument( + "--crop_anchor", + choices=["center", "top", "bottom"], + default="center", + help="Only used when --fit crop. Vertical crop anchor.", + ) + p.add_argument( + "--pad_color", + choices=["magenta", "green", "black"], + default="magenta", + help="Padding color used when --fit contain.", + ) + + # NEW: zoom + p.add_argument( + "--zoom", + type=float, + default=1.0, + help=( + "Zoom factor applied to each frame BEFORE fit. " + "1.0=no change, >1.0 zooms in (subject bigger), <1.0 zooms out." + ), + ) + + # Sampling strategy + p.add_argument( + "--sample_by", + choices=["time", "frame"], + default="frame", + help="How to sample frames: 'frame' (robust) or 'time' (uses CAP_PROP_POS_MSEC). Default: frame", + ) + + # Time-based sampling controls + p.add_argument( + "--time_endpoint_mode", + choices=["exclude", "include", "clamp"], + default="exclude", + help=( + "Only used when --sample_by time. " + "'exclude' never samples the exact end time (recommended). " + "'include' includes duration endpoint (may fail near end). " + "'clamp' includes endpoint but clamps to duration-epsilon." + ), + ) + p.add_argument( + "--time_epsilon_ms", + type=float, + default=1.0, + help="Only used when --sample_by time and endpoint mode uses clamping. Default: 1.0ms", + ) + p.add_argument( + "--time_seek_backoff_ms", + type=float, + nargs="*", + default=[0.0, 5.0, 10.0, 20.0, 40.0, 80.0, 160.0], + help="Only used when --sample_by time. Backoff steps (ms) tried if read fails at target time.", + ) + + # Frame-index sampling controls + p.add_argument( + "--frame_endpoint_mode", + choices=["exclude_last", "include_last"], + default="exclude_last", + help=( + "Only used when --sample_by frame. " + "'exclude_last' avoids sampling the last reported frame index (recommended). " + "'include_last' may fail on some MP4s." + ), + ) + p.add_argument( + "--frame_seek_backoff", + type=int, + nargs="*", + default=[0, 1, 2, 3, 5, 8, 13], + help="Only used when --sample_by frame. If a frame read fails, try idx-backoff steps (in frames).", + ) + + # MOG2 tuning + p.add_argument("--mog2_learning_rate", type=float, default=0.001, help="MOG2 learning rate per frame") + p.add_argument("--mog2_warmup", type=int, default=0, help="Number of initial sampled frames used for warmup") + p.add_argument( + "--mog2_freeze_after_warmup", + action="store_true", + help="After warmup, freeze background model (learningRate=0).", + ) + + p.add_argument( + "--mode", + choices=["indexed", "rgb"], + default="indexed", + help="Output mode: indexed (Civ3 PCX) or rgb (edit-friendly)", + ) + p.add_argument( + "--resample", + choices=["nearest", "bilinear", "bicubic", "lanczos"], + default="nearest", + help="Resampling method for resizing frames (default: nearest / no interpolation)", + ) + + # Cutout (Option B) + p.add_argument("--cutout", action="store_true", help="Enable background removal (MOG2)") + p.add_argument("--mog2_history", type=int, default=200, help="MOG2 history length") + p.add_argument("--mog2_var_threshold", type=float, default=16.0, help="MOG2 varThreshold") + p.add_argument("--mog2_detect_shadows", action="store_true", help="Enable MOG2 shadow detection") + p.add_argument("--cutout_center_frac", type=float, default=0.85, help="Center region fraction (0-1)") + p.add_argument("--cutout_keep_largest", action="store_true", help="Keep only the largest component") + p.add_argument("--cutout_min_area", type=int, default=200, help="Minimum component area to keep") + p.add_argument("--cutout_dilate", type=int, default=2, help="Dilate mask pixels") + p.add_argument("--cutout_blur", type=int, default=3, help="Blur kernel size for mask (odd; 0 disables)") + + # Debug + p.add_argument("--print_video_info", action="store_true", help="Print FPS/frame_count/duration estimates") + + return p.parse_args() + + +def pil_resample(name: str) -> int: + if name == "nearest": + return Image.NEAREST + if name == "bilinear": + return Image.BILINEAR + if name == "bicubic": + return Image.BICUBIC + if name == "lanczos": + return Image.LANCZOS + raise ValueError(name) + + +def pad_color_rgb(name: str) -> Tuple[int, int, int]: + if name == "magenta": + return MAGENTA + if name == "green": + return GREEN + if name == "black": + return BLACK + raise ValueError(name) + + +def apply_zoom_center(im: Image.Image, zoom: float, resample: int) -> Image.Image: + """ + Zoom around center while keeping the SAME output dimensions as input. + - zoom > 1: zoom in (subject bigger) + - zoom < 1: zoom out (subject smaller; adds border replicated by scaling down + padding) + This is done BEFORE fit/crop/contain. + """ + if zoom <= 0: + raise ValueError("--zoom must be > 0") + if abs(zoom - 1.0) < 1e-9: + return im + + w, h = im.size + # Scale image + nw = max(1, int(round(w * zoom))) + nh = max(1, int(round(h * zoom))) + scaled = im.resize((nw, nh), resample=resample) + + if zoom >= 1.0: + # Center-crop back to original size + left = (nw - w) // 2 + top = (nh - h) // 2 + return scaled.crop((left, top, left + w, top + h)) + + # zoom < 1.0: center-pad back to original size + out = Image.new(im.mode, (w, h), color=BLACK if im.mode == "RGB" else 0) + ox = (w - nw) // 2 + oy = (h - nh) // 2 + out.paste(scaled, (ox, oy)) + return out + + +def center_crop_to_aspect(im: Image.Image, target_w: int, target_h: int, crop_anchor: str) -> Image.Image: + w, h = im.size + target_aspect = target_w / target_h + src_aspect = w / h + + if abs(src_aspect - target_aspect) < 1e-9: + return im + + if src_aspect > target_aspect: + new_w = int(round(h * target_aspect)) + left = (w - new_w) // 2 + return im.crop((left, 0, left + new_w, h)) + else: + new_h = int(round(w / target_aspect)) + if crop_anchor == "top": + top = 0 + elif crop_anchor == "bottom": + top = h - new_h + else: + top = (h - new_h) // 2 + return im.crop((0, top, w, top + new_h)) + + +def fit_contain( + im: Image.Image, + target_w: int, + target_h: int, + pad_rgb: Tuple[int, int, int], + resample: int, +) -> Image.Image: + """Scale to fit entirely within target, preserve aspect, then pad to exact size.""" + w, h = im.size + if w <= 0 or h <= 0: + return Image.new("RGB", (target_w, target_h), color=pad_rgb) + + scale = min(target_w / w, target_h / h) + nw = max(1, int(round(w * scale))) + nh = max(1, int(round(h * scale))) + + resized = im.resize((nw, nh), resample=resample) + out = Image.new("RGB", (target_w, target_h), color=pad_rgb) + ox = (target_w - nw) // 2 + oy = (target_h - nh) // 2 + out.paste(resized, (ox, oy)) + return out + + +def get_duration_ms(cap: cv2.VideoCapture) -> float: + fps = cap.get(cv2.CAP_PROP_FPS) or 0.0 + frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0.0 + if fps > 0 and frame_count > 0: + return max(0.0, (frame_count - 1.0) / fps * 1000.0) + + cur = cap.get(cv2.CAP_PROP_POS_FRAMES) + cap.set(cv2.CAP_PROP_POS_AVI_RATIO, 1.0) + _ok, _ = cap.read() + end_ms = cap.get(cv2.CAP_PROP_POS_MSEC) or 0.0 + cap.set(cv2.CAP_PROP_POS_FRAMES, cur) + return max(0.0, end_ms) + + +def linspace_times(start_ms: float, end_ms: float, n: int, endpoint_mode: str, eps_ms: float) -> List[float]: + if n <= 0: + return [] + if end_ms <= start_ms: + return [float(start_ms)] * n + if n == 1: + return [float(start_ms)] + + if endpoint_mode == "exclude": + return [float(x) for x in np.linspace(float(start_ms), float(end_ms), n, endpoint=False)] + if endpoint_mode == "include": + return [float(x) for x in np.linspace(float(start_ms), float(end_ms), n, endpoint=True)] + if endpoint_mode == "clamp": + hi = max(float(start_ms), float(end_ms) - float(max(0.0, eps_ms))) + return [float(x) for x in np.linspace(float(start_ms), hi, n, endpoint=True)] + raise ValueError(endpoint_mode) + + +def read_frame_at_time_with_backoff( + cap: cv2.VideoCapture, + t_ms: float, + backoffs_ms: List[float], +) -> np.ndarray: + for back in backoffs_ms: + t_try = max(0.0, float(t_ms) - float(back)) + cap.set(cv2.CAP_PROP_POS_MSEC, t_try) + ok, frame_bgr = cap.read() + if ok and frame_bgr is not None: + return cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + raise RuntimeError(f"Failed to read frame near time {t_ms:.2f}ms") + + +def sample_frames_by_time( + cap: cv2.VideoCapture, + cols: int, + start_ms: float, + end_ms: float, + endpoint_mode: str, + eps_ms: float, + backoffs_ms: List[float], + print_info: bool, +) -> List[np.ndarray]: + duration_ms = get_duration_ms(cap) + times = linspace_times(start_ms, end_ms, cols, endpoint_mode=endpoint_mode, eps_ms=eps_ms) + + if print_info: + fps = cap.get(cv2.CAP_PROP_FPS) or 0.0 + frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0.0 + print(f"[video] fps={fps:.3f} frame_count={frame_count} duration_ms≈{duration_ms:.2f}") + print(f"[sample] selected window: {start_ms:.2f}ms .. {end_ms:.2f}ms") + if times: + print(f"[sample] time range: {times[0]:.2f}ms .. {times[-1]:.2f}ms (count={len(times)})") + + frames: List[np.ndarray] = [] + for t_ms in times: + frames.append(read_frame_at_time_with_backoff(cap, t_ms, backoffs_ms=backoffs_ms)) + return frames + + +def read_frame_at_index_with_backoff( + cap: cv2.VideoCapture, + idx: int, + frame_count: int, + backoffs: List[int], +) -> np.ndarray: + for b in backoffs: + j = int(idx) - int(b) + if j < 0 or j >= int(frame_count): + continue + cap.set(cv2.CAP_PROP_POS_FRAMES, int(j)) + ok, frame_bgr = cap.read() + if ok and frame_bgr is not None: + return cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + raise RuntimeError(f"Failed to read frame index {int(idx)} with backoff (frame_count={int(frame_count)})") + + +def sample_frames_by_index( + cap: cv2.VideoCapture, + cols: int, + start_ms: float, + end_ms: float, + print_info: bool, + endpoint_mode: str, + backoffs: List[int], +) -> List[np.ndarray]: + frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0) + fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0) + + if frame_count <= 0: + duration_ms = get_duration_ms(cap) + if print_info: + print(f"[video] frame_count unavailable; fps={fps:.3f} duration_ms≈{duration_ms:.2f}. Falling back to time sampling.") + return sample_frames_by_time( + cap, + cols=cols, + start_ms=start_ms, + end_ms=end_ms, + endpoint_mode="exclude", + eps_ms=1.0, + backoffs_ms=[0.0, 5.0, 10.0, 20.0, 40.0, 80.0, 160.0], + print_info=print_info, + ) + + duration_ms = (frame_count - 1) / fps * 1000.0 if fps > 0 else float("nan") + + hi = frame_count - 1 + if endpoint_mode == "exclude_last": + hi = max(0, frame_count - 2) + lo = 0 + if fps > 0.0: + lo = max(0, min(hi, int(math.ceil((float(start_ms) / 1000.0) * fps)))) + hi = min(hi, int(math.floor((float(end_ms) / 1000.0) * fps))) + if lo > hi: + raise ValueError( + f"Selected time window has no readable frame indices: start_ms={start_ms:.2f}, end_ms={end_ms:.2f}" + ) + + if cols == 1: + idxs = np.array([lo], dtype=int) + else: + idxs = np.linspace(lo, hi, cols, endpoint=True).astype(int) + + if print_info: + dur_str = f"{duration_ms:.2f}" if duration_ms == duration_ms else "unknown" + print(f"[video] fps={fps:.3f} frame_count={frame_count} duration_ms≈{dur_str}") + print(f"[sample] selected window: {start_ms:.2f}ms .. {end_ms:.2f}ms") + if len(idxs) > 0: + print(f"[sample] frame index range: {int(idxs[0])} .. {int(idxs[-1])} (count={len(idxs)})") + if endpoint_mode == "exclude_last": + print(f"[sample] exclude_last enabled; max sampled index is {int(hi)} (reported last is {frame_count - 1})") + + frames: List[np.ndarray] = [] + for idx in idxs: + frames.append(read_frame_at_index_with_backoff(cap, int(idx), frame_count=frame_count, backoffs=backoffs)) + return frames + + +# -------------------------- +# Option B: MOG2 cutout helpers +# -------------------------- + +def center_gate_mask(h: int, w: int, frac: float) -> np.ndarray: + frac = float(np.clip(frac, 0.05, 1.0)) + cw = max(1, int(round(w * frac))) + ch = max(1, int(round(h * frac))) + x0 = (w - cw) // 2 + y0 = (h - ch) // 2 + mask = np.zeros((h, w), dtype=np.uint8) + mask[y0:y0 + ch, x0:x0 + cw] = 255 + return mask + + +def keep_largest_component(mask: np.ndarray) -> np.ndarray: + num, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8) + if num <= 1: + return mask + best_i = 1 + best_area = int(stats[1, cv2.CC_STAT_AREA]) + for i in range(2, num): + area = int(stats[i, cv2.CC_STAT_AREA]) + if area > best_area: + best_area = area + best_i = i + out = np.zeros_like(mask) + out[labels == best_i] = 255 + return out + + +def filter_small_components(mask: np.ndarray, min_area: int) -> np.ndarray: + num, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8) + out = np.zeros_like(mask) + for i in range(1, num): + area = int(stats[i, cv2.CC_STAT_AREA]) + if area >= int(min_area): + out[labels == i] = 255 + return out + + +def cleanup_mask(mask: np.ndarray, dilate_iters: int) -> np.ndarray: + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) + mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k, iterations=2) + mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, k, iterations=1) + if dilate_iters and int(dilate_iters) > 0: + mask = cv2.dilate(mask, k, iterations=int(dilate_iters)) + return mask + + +def mog2_cutout_frames( + frames_rgb: List[np.ndarray], + history: int, + var_threshold: float, + detect_shadows: bool, + center_frac: float, + keep_largest: bool, + min_area: int, + dilate_iters: int, + blur_k: int, + learning_rate: float, + warmup: int, + freeze_after_warmup: bool, +) -> List[np.ndarray]: + subtractor = cv2.createBackgroundSubtractorMOG2( + history=int(history), + varThreshold=float(var_threshold), + detectShadows=bool(detect_shadows), + ) + + out_frames: List[np.ndarray] = [] + + for i, fr in enumerate(frames_rgb): + if i < int(warmup): + lr = 0.5 + else: + if freeze_after_warmup and int(warmup) > 0: + lr = 0.0 + else: + lr = float(learning_rate) + + fg = subtractor.apply(fr, learningRate=lr) + + # If shadows enabled, OpenCV uses 127 for shadows; treat as background. + fg = (fg == 255).astype(np.uint8) * 255 + + h, w = fg.shape + gate = center_gate_mask(h, w, center_frac) + fg = cv2.bitwise_and(fg, gate) + + fg = cleanup_mask(fg, dilate_iters=dilate_iters) + + if keep_largest: + fg = keep_largest_component(fg) + else: + fg = filter_small_components(fg, min_area=min_area) + + if blur_k and int(blur_k) > 0: + k = int(blur_k) + if k % 2 == 0: + k += 1 + fg = cv2.GaussianBlur(fg, (k, k), 0) + fg = (fg > 127).astype(np.uint8) * 255 + + out = fr.copy() + out[fg == 0] = MAGENTA + out_frames.append(out) + + return out_frames + + +# -------------------------- +# Palette + sheet building +# -------------------------- + +def build_sample_palette_from_frames(frames_rgb: List[Image.Image]) -> List[Tuple[int, int, int]]: + if not frames_rgb: + return [] + + thumbs: List[Image.Image] = [] + for im in frames_rgb: + w, h = im.size + scale = max(1, math.ceil(max(w, h) / 128)) + tw, th = max(1, w // scale), max(1, h // scale) + thumbs.append(im.resize((tw, th), resample=Image.NEAREST)) + + total_w = sum(t.size[0] for t in thumbs) + max_h = max(t.size[1] for t in thumbs) + mosaic = Image.new("RGB", (max(1, total_w), max(1, max_h)), color=BLACK) + + x = 0 + for t in thumbs: + mosaic.paste(t, (x, 0)) + x += t.size[0] + + q = mosaic.quantize(colors=SAMPLED_COLORS, method=Image.MEDIANCUT, dither=Image.NONE) + pal = q.getpalette() or [] + + colors: List[Tuple[int, int, int]] = [] + for i in range(SAMPLED_COLORS): + base = i * 3 + if base + 2 >= len(pal): + break + r, g, b = pal[base: base + 3] + colors.append((int(r), int(g), int(b))) + + seen = set() + uniq: List[Tuple[int, int, int]] = [] + for c in colors: + if c not in seen: + uniq.append(c) + seen.add(c) + return uniq + + +def make_civ3_palette(sampled: List[Tuple[int, int, int]]) -> List[int]: + pal: List[int] = [] + + for _ in range(RESERVED_CIV_COLORS): + pal += [*WHITE] + + sampled = sampled[:SAMPLED_COLORS] + if len(sampled) < SAMPLED_COLORS: + sampled = sampled + [BLACK] * (SAMPLED_COLORS - len(sampled)) + for (r, g, b) in sampled: + pal += [int(r), int(g), int(b)] + + pal += [*GREEN] # 254 + pal += [*MAGENTA] # 255 + assert len(pal) == 768 + return pal + + +def build_sheet_rgb(frames: List[Image.Image], cols: int, slot_w: int, slot_h: int) -> Image.Image: + rows = 8 + sheet_w = cols * slot_w + (cols + 1) + sheet_h = rows * slot_h + (rows + 1) + + sheet = Image.new("RGB", (sheet_w, sheet_h), color=GREEN) + magenta_inner = Image.new("RGB", (slot_w, slot_h), color=MAGENTA) + + def cell_xy(col: int, row: int) -> Tuple[int, int]: + x = 1 + col * (slot_w + 1) + y = 1 + row * (slot_h + 1) + return x, y + + for c in range(cols): + x, y = cell_xy(c, 0) + sheet.paste(frames[c], (x, y)) + for r in range(1, rows): + x2, y2 = cell_xy(c, r) + sheet.paste(magenta_inner, (x2, y2)) + + return sheet + + +def validate_out_path(out_path: str) -> None: + if os.path.isdir(out_path): + raise ValueError(f"--out must be a file path, not a directory: {out_path}") + parent = os.path.dirname(out_path) or "." + if parent and not os.path.exists(parent): + os.makedirs(parent, exist_ok=True) + + +def main() -> None: + args = parse_args() + if args.columns <= 0: + raise ValueError("--columns must be > 0") + if args.zoom <= 0: + raise ValueError("--zoom must be > 0") + if args.start_time < 0: + raise ValueError("--start_time must be >= 0") + if args.end_time is not None and args.end_time < 0: + raise ValueError("--end_time must be >= 0") + + validate_out_path(args.out) + + resample = pil_resample(args.resample) + pad_rgb = pad_color_rgb(args.pad_color) + + cap = cv2.VideoCapture(args.mp4) + if not cap.isOpened(): + raise RuntimeError(f"Could not open video: {args.mp4}") + + duration_ms = get_duration_ms(cap) + start_ms = float(args.start_time) * 1000.0 + end_ms = duration_ms if args.end_time is None else float(args.end_time) * 1000.0 + start_ms = max(0.0, min(start_ms, duration_ms)) + end_ms = max(0.0, min(end_ms, duration_ms)) + if end_ms < start_ms: + raise ValueError( + f"--end_time ({end_ms / 1000.0:.3f}s) must be >= --start_time ({start_ms / 1000.0:.3f}s)" + ) + + try: + if args.sample_by == "frame": + raw_frames_np = sample_frames_by_index( + cap, + cols=args.columns, + start_ms=start_ms, + end_ms=end_ms, + print_info=bool(args.print_video_info), + endpoint_mode=args.frame_endpoint_mode, + backoffs=[int(x) for x in (args.frame_seek_backoff or [0])], + ) + else: + raw_frames_np = sample_frames_by_time( + cap, + cols=args.columns, + start_ms=start_ms, + end_ms=end_ms, + endpoint_mode=args.time_endpoint_mode, + eps_ms=float(args.time_epsilon_ms), + backoffs_ms=[float(x) for x in (args.time_seek_backoff_ms or [0.0])], + print_info=bool(args.print_video_info), + ) + finally: + cap.release() + + if args.cutout: + processed_np = mog2_cutout_frames( + raw_frames_np, + history=args.mog2_history, + var_threshold=args.mog2_var_threshold, + detect_shadows=args.mog2_detect_shadows, + center_frac=args.cutout_center_frac, + keep_largest=True if args.cutout_keep_largest else False, + min_area=args.cutout_min_area, + dilate_iters=args.cutout_dilate, + blur_k=args.cutout_blur, + learning_rate=args.mog2_learning_rate, + warmup=args.mog2_warmup, + freeze_after_warmup=args.mog2_freeze_after_warmup, + ) + else: + processed_np = raw_frames_np + + frames: List[Image.Image] = [] + for fr_np in processed_np: + im = Image.fromarray(fr_np, mode="RGB") + + # NEW: apply zoom before fit + im = apply_zoom_center(im, zoom=float(args.zoom), resample=resample) + + if args.fit == "crop": + im = center_crop_to_aspect(im, args.slot_w, args.slot_h, args.crop_anchor) + im = im.resize((args.slot_w, args.slot_h), resample=resample) + else: + # contain: NEVER crops; prevents “fish bottom cut off” + im = fit_contain(im, args.slot_w, args.slot_h, pad_rgb=pad_rgb, resample=resample) + + frames.append(im) + + sheet_rgb = build_sheet_rgb(frames, args.columns, args.slot_w, args.slot_h) + + if args.mode == "rgb": + sheet_rgb.save(args.out, format="PCX") + print(f"Saved RGB PCX: {args.out}") + return + + sampled_colors = build_sample_palette_from_frames(frames) + palette_list = make_civ3_palette(sampled_colors) + + pal_img = Image.new("P", (16, 16)) + pal_img.putpalette(palette_list) + + indexed = sheet_rgb.quantize(palette=pal_img, dither=Image.NONE) + + ext = os.path.splitext(args.out)[1].lower() + if ext != ".pcx": + print("Note: --mode indexed is intended for .pcx output (use .pcx extension).") + + indexed.save(args.out, format="PCX") + print(f"Saved indexed PCX: {args.out}") + + +if __name__ == "__main__": + main() diff --git a/DayNight/pcx_sheet_to_civ3_flc_flicker.py b/DayNight/pcx_sheet_to_civ3_flc_flicker.py new file mode 100644 index 00000000..fafd3ade --- /dev/null +++ b/DayNight/pcx_sheet_to_civ3_flc_flicker.py @@ -0,0 +1,863 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +pcx_lights_sheet_to_civ3_flc_overlay.py + +Generate Civ3-compatible-ish FLC overlay animations from a Civ3-style *_lights.pcx annotation sheet. +This script IMPORTS civ3_city_lights.py to reuse the exact glow math / palette rules. + +Input: PCX sheet (ideally indexed P mode) where specific "light key" colors mark light sources. +Output: one .flc per (row, col) cell, containing: + - one BYTE_RUN keyframe (not counted in header frame count; FLICster-friendly) + - N animation frames (LITERAL) with flicker + - optional ring frame + +Defaults: cell size 128x64 + +Notes: +- We render a TRANSPARENT overlay: background becomes MAGENTA (255,0,255) at palette index 255. +- We do NOT need a base city image; this is just the glow overlay from the annotation mask. +""" + +import argparse +import math +import os +import struct +from dataclasses import dataclass +from typing import List, Tuple, Dict, Optional + +from PIL import Image, ImageChops + +# Import your compositor module (must be accessible) +import civ3_city_lights as c3 + + +# ----------------------------- +# FLC constants +# ----------------------------- +FLC_MAGIC = 0xAF12 +CHUNK_FRAME = 0xF1FA +CHUNK_COLOR_256 = 4 +CHUNK_BYTE_RUN = 15 +CHUNK_LITERAL = 16 + +CIV3_CREATOR = 0xF1F1F2F2 +CIV3_SPEED = 4 # Civ3 convention + + +# ----------------------------- +# Packing helpers +# ----------------------------- +def clamp8(v: float) -> int: + if v <= 0: + return 0 + if v >= 255: + return 255 + return int(v + 0.5) + + +def pack_u16(v: int) -> bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + return struct.pack(" bytes: + pal = im_p.getpalette() or [] + if len(pal) < 768: + pal = pal + [0] * (768 - len(pal)) + return bytes(pal[:768]) + + +# ----------------------------- +# FLC chunk builders +# ----------------------------- +def make_color_256_payload(pal768: bytes) -> bytes: + """ + COLOR_256 payload packetized: + u16 packet_count + packet: u8 skip, u8 count(0=>256), RGB*count + """ + if len(pal768) != 768: + raise ValueError("Palette must be 768 bytes") + return struct.pack(" bytes: + size = 6 + len(payload) + return pack_u32(size) + pack_u16(chunk_type) + payload + +def make_frame_chunk(subchunks: List[bytes]) -> bytes: + payload = b"".join(subchunks) + size = 16 + len(payload) + return pack_u32(size) + pack_u16(CHUNK_FRAME) + pack_u16(len(subchunks)) + (b"\x00" * 8) + payload + +def make_byte_run_payload(pix: bytes, w: int, h: int) -> bytes: + """ + FLI_BRUN (type 15) payload. + Per scanline: + u8 packets (often ignored) + signed size packets until line is full: + size > 0 : repeat next byte size times + size < 0 : copy next abs(size) literal bytes + """ + if len(pix) != w * h: + raise ValueError("BYTE_RUN payload size mismatch") + + out = bytearray() + for y in range(h): + row = pix[y * w:(y + 1) * w] + out.append(0) # legacy packet count + x = 0 + while x < w: + # RLE run + run = 1 + while x + run < w and run < 127 and row[x + run] == row[x]: + run += 1 + if run >= 2: + out.append(run & 0xFF) + out.append(row[x]) + x += run + continue + + # Literal run + start = x + x += 1 + while x < w and (x - start) < 127: + look = 1 + while x + look < w and look < 127 and row[x + look] == row[x]: + look += 1 + if look >= 2: + break + x += 1 + + n = x - start + out.append((256 - n) & 0xFF) # negative signed byte as u8 + out.extend(row[start:x]) + + return bytes(out) + + +# ----------------------------- +# Civ3 custom header (FlicAnimHeader) tail +# ----------------------------- +@dataclass +class Civ3FlicAnimHeader: + num_anims: int + anim_length: int + x_offset: int + y_offset: int + xs_orig: int + ys_orig: int + anim_time_ms: int + directions: int + + def pack_40_bytes(self) -> bytes: + core = ( + pack_u32(28) + + pack_i32(0) + + pack_u16(self.num_anims) + + pack_u16(self.anim_length) + + pack_u16(self.x_offset) + + pack_u16(self.y_offset) + + pack_u16(self.xs_orig) + + pack_u16(self.ys_orig) + + pack_u32(self.anim_time_ms) + + pack_i32(self.directions) + ) + if len(core) != 28: + raise AssertionError("Civ3FlicAnimHeader core must be 28 bytes") + return core + (b"\x00" * 12) + + +def build_flc_header_128( + total_file_size: int, + frames_without_ring: int, + w: int, + h: int, + speed_ms: int, + creator: int, + oframe1: int, + oframe2: int, + civ3_tail_40: bytes, +) -> bytes: + if len(civ3_tail_40) != 40: + raise ValueError("civ3_tail_40 must be 40 bytes") + + hdr = bytearray(b"\x00" * 128) + hdr[0:4] = pack_u32(total_file_size) + hdr[4:6] = pack_u16(FLC_MAGIC) + hdr[6:8] = pack_u16(frames_without_ring) + hdr[8:10] = pack_u16(w) + hdr[10:12] = pack_u16(h) + hdr[12:14] = pack_u16(8) # depth + hdr[14:16] = pack_u16(0x0003) # flags + hdr[16:20] = pack_u32(speed_ms) + hdr[20:22] = pack_u16(0) + + hdr[26:30] = pack_u32(creator) + hdr[38:40] = pack_u16(1) # aspectx + hdr[40:42] = pack_u16(1) # aspecty + + hdr[80:84] = pack_u32(oframe1) + hdr[84:88] = pack_u32(oframe2) + + hdr[88:128] = civ3_tail_40 + return bytes(hdr) + + +def write_flc( + out_path: str, + frames_p: List[Image.Image], # P-mode frames (indexed), same size + fps: float, + civ_num_anims: int, + civ_directions_bitmask: int, + civ_x_offset: int, + civ_y_offset: int, + civ_xs_orig: int, + civ_ys_orig: int, + include_ring_frame: bool, + flc_speed_ms: int, +) -> None: + if not frames_p: + raise ValueError("No frames") + w, h = frames_p[0].size + for im in frames_p: + if im.mode != "P": + raise ValueError("All frames must be 'P' mode") + if im.size != (w, h): + raise ValueError("All frames must have identical size") + + anim_length = len(frames_p) + anim_time_ms = int(round(anim_length * 1000.0 / max(0.001, fps))) + + civ_hdr = Civ3FlicAnimHeader( + num_anims=civ_num_anims, + anim_length=anim_length, + x_offset=civ_x_offset, + y_offset=civ_y_offset, + xs_orig=civ_xs_orig, + ys_orig=civ_ys_orig, + anim_time_ms=anim_time_ms, + directions=civ_directions_bitmask, + ).pack_40_bytes() + + pal768 = palette_bytes_768(frames_p[0]) + + # Build chunks: + # - one BRUN keyframe with palette (not counted in header frames) + # - then LITERAL frames (counted) + frame_chunks: List[bytes] = [] + + key_pix = frames_p[0].tobytes() + key_subchunks = [ + make_subchunk(CHUNK_BYTE_RUN, make_byte_run_payload(key_pix, w, h)), + make_subchunk(CHUNK_COLOR_256, make_color_256_payload(pal768)), + ] + frame_chunks.append(make_frame_chunk(key_subchunks)) + + for im in frames_p: + frame_chunks.append(make_frame_chunk([make_subchunk(CHUNK_LITERAL, im.tobytes())])) + + body = b"".join(frame_chunks) + + if include_ring_frame: + ring_pix = frames_p[0].tobytes() + ring = make_frame_chunk([make_subchunk(CHUNK_BYTE_RUN, make_byte_run_payload(ring_pix, w, h))]) + body += ring + + total_size = 128 + len(body) + + # Civ3 expects header frames = num_anims * anim_length (excluding keyframe and ring) + frames_without_ring = civ_num_anims * anim_length + + oframe1 = 128 + oframe2 = 0 # many Civ3-related tools tolerate/expect 0 here + + header = build_flc_header_128( + total_file_size=total_size, + frames_without_ring=frames_without_ring, + w=w, + h=h, + speed_ms=flc_speed_ms, + creator=CIV3_CREATOR, + oframe1=oframe1, + oframe2=oframe2, + civ3_tail_40=civ_hdr, + ) + + os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) + with open(out_path, "wb") as f: + f.write(header) + f.write(body) + + +# ----------------------------- +# Overlay glow rendering (reusing civ3_city_lights functions) +# ----------------------------- +def _flicker_multiplier(frame_i: int, frames: int, fps: float, phase: float, amp: float, hz1: float, hz2: float) -> float: + t = frame_i / max(1.0, fps) + a = math.sin(2.0 * math.pi * hz1 * t + phase) + b = math.sin(2.0 * math.pi * hz2 * t + phase * 1.37 + 0.9) + v = math.tanh((0.65 * a + 0.35 * b) * 1.2) + # Keep weight in [0, 1] for build_glow_maps(wtime=...). + # Center at 0.5 so both dimming and brightening are represented. + return max(0.0, min(1.0, 0.5 + amp * v)) + +def _phase_for_cell(r: int, c: int, seed: int) -> float: + # deterministic cell-level phase + v = (r * 10007 + c * 10009 + seed * 700001) & 0xFFFFFFFF + v ^= (v >> 16) + frac = (v & 0x00FFFFFF) / float(0x01000000) + return frac * 2.0 * math.pi + + +def _coord_hash_u32(x: int, y: int, seed: int) -> int: + v = (x * 374761393 + y * 668265263 + seed * 700001) & 0xFFFFFFFF + v ^= (v >> 13) & 0xFFFFFFFF + v = (v * 1274126177) & 0xFFFFFFFF + v ^= (v >> 16) & 0xFFFFFFFF + return v + + +def apply_per_pixel_flicker( + overlay_rgba: Image.Image, + frame_i: int, + fps: float, + amp: float, + hz1: float, + hz2: float, + seed: int, +) -> Image.Image: + """ + Subtle pixel-level modulation so lights don't all move in lockstep. + Background magenta is preserved exactly. + """ + w, h = overlay_rgba.size + out = overlay_rgba.copy() + px = out.load() + t = frame_i / max(1.0, fps) + + for y in range(h): + for x in range(w): + r, g, b, a = px[x, y] + if (r, g, b) == c3.MAGENTA: + continue + ph = (_coord_hash_u32(x, y, seed) & 0x00FFFFFF) / float(0x01000000) * (2.0 * math.pi) + s1 = math.sin(2.0 * math.pi * hz1 * t + ph) + s2 = math.sin(2.0 * math.pi * hz2 * t + ph * 1.37 + 0.9) + v = math.tanh((0.65 * s1 + 0.35 * s2) * 1.2) + mult = max(0.0, 1.0 + amp * v) + px[x, y] = (clamp8(r * mult), clamp8(g * mult), clamp8(b * mult), a) + + return out + + +def apply_global_flicker_gain( + overlay_rgba: Image.Image, + weight_0_1: float, + strength: float, +) -> Image.Image: + """ + Apply a frame-wide gain to non-magenta pixels. + This guarantees visible frame-to-frame movement even after quantization. + """ + w, h = overlay_rgba.size + out = overlay_rgba.copy() + px = out.load() + + # Map weight [0,1] to gain around 1.0 with asymmetric swing: + # stronger dimming than brightening avoids clipping bright cores to 255 + # across all frames, which can flatten visible flicker after quantization. + delta = (weight_0_1 * 2.0 - 1.0) + s = max(0.0, strength) + if delta >= 0.0: + gain = 1.0 + (0.18 * s * delta) + else: + gain = 1.0 + (0.70 * s * delta) + + for y in range(h): + for x in range(w): + r, g, b, a = px[x, y] + if (r, g, b) == c3.MAGENTA: + continue + px[x, y] = (clamp8(r * gain), clamp8(g * gain), clamp8(b * gain), a) + + return out + + +def ensure_adjacent_frames_differ(frames_p: List[Image.Image], transparent_index: int) -> int: + """ + Prevent accidental frame collapse after quantization by nudging several + non-transparent pixels if two adjacent indexed frames are byte-identical. + """ + if len(frames_p) < 2: + return 0 + + adjusted = 0 + + for i in range(1, len(frames_p)): + if frames_p[i].tobytes() != frames_p[i - 1].tobytes(): + continue + + fr = frames_p[i].copy() + px = fr.load() + w, h = fr.size + seed = (i * 1103515245 + w * 12345 + h * 2654435761) & 0xFFFFFFFF + + changed = 0 + total = w * h + target_changes = max(8, min(64, total // 512)) + for n in range(total): + p = (seed + n * 2654435761) % total + x = p % w + y = p // w + idx = int(px[x, y]) + if idx == transparent_index: + continue + + # Keep index 255 reserved for transparency. + px[x, y] = (idx + 1) % 255 + changed += 1 + if changed >= target_changes: + break + + if changed > 0: + frames_p[i] = fr + adjusted += 1 + + return adjusted + +def render_overlay_frame_rgba( + mask_source_rgb: Image.Image, + keys: List[Tuple[int, int, int]], + styles: Dict[Tuple[int, int, int], Dict[str, object]], + intensity: float, + # global defaults (same semantics as civ3_city_lights): + core_radius: float, + halo_radius: float, + core_gain: float, + halo_gain: float, + core_color: Tuple[int, int, int], + glow_color: Tuple[int, int, int], + size_boost: float, + size_radius: float, + size_gamma: float, + highlight_gain: float, + blend_mode: str, + halo_sep: float, + halo_gamma: float, +) -> Image.Image: + """ + Produce an RGBA overlay where background is MAGENTA (will become transparent). + We reuse civ3_city_lights primitives to match your existing glow appearance. + """ + w, h = mask_source_rgb.size + + # Start with black comp for accurate glow blending, plus an alpha union mask. + comp = Image.new("RGB", (w, h), (0, 0, 0)) + union_alpha = Image.new("L", (w, h), 0) + + # Ensure styled keys included + keys_set = set(keys) + keys_set.update(styles.keys()) + keys_order = list(keys_set) + + for key_rgb in keys_order: + mask_bin = c3.color_equal(mask_source_rgb, key_rgb) + if mask_bin.getbbox() is None: + continue + + st = styles.get(key_rgb, {}) + k_core_color = st.get("core_color", core_color) + k_glow_color = st.get("glow_color", glow_color) + k_core_gain = float(st.get("core_gain", core_gain)) + k_halo_gain = float(st.get("halo_gain", halo_gain)) + k_core_rad = float(st.get("core_radius", core_radius)) + k_halo_rad = float(st.get("halo_radius", halo_radius)) + k_halo_sep = float(st.get("halo_sep", halo_sep)) + k_halo_gamma = float(st.get("halo_gamma", halo_gamma)) + k_size_boost = float(st.get("size_boost", size_boost)) + k_size_radius= float(st.get("size_radius", size_radius)) + k_size_gamma = float(st.get("size_gamma", size_gamma)) + k_highlight = float(st.get("highlight", highlight_gain)) + k_blend_mode = str(st.get("blend_mode", blend_mode)).lower() + + # For overlays, interior mask is full 255 (no clipping to city silhouette) + interior = Image.new("L", (w, h), 255) + + core_alpha, halo_alpha = c3.build_glow_maps( + mask_bin=mask_bin, + interior_mask=interior, + wtime=max(0.0, min(1.0, intensity)), # intensity acts like time weight + core_radius=k_core_rad, + halo_radius=k_halo_rad, + core_gain=k_core_gain, + halo_gain=k_halo_gain, + size_boost=k_size_boost, + size_radius=k_size_radius, + size_gamma=k_size_gamma, + halo_sep=k_halo_sep, + halo_gamma=k_halo_gamma, + ) + + core_layer = c3.layer_from_alpha(k_core_color, core_alpha) + halo_layer = c3.layer_from_alpha(k_glow_color, halo_alpha) + + if k_blend_mode == "add": + comp = ImageChops.add(comp, core_layer, scale=1.0) + comp = ImageChops.add(comp, halo_layer, scale=1.0) + else: + comp = c3.screen_blend(comp, core_layer) + comp = c3.screen_blend(comp, halo_layer) + + # Union alpha for transparency decision + union_alpha = ImageChops.lighter(union_alpha, core_alpha) + union_alpha = ImageChops.lighter(union_alpha, halo_alpha) + + if k_highlight > 0.0: + hl = c3.scale_L(core_alpha, k_highlight) + # add highlight to comp + comp = ImageChops.add( + comp, + Image.composite( + Image.new("RGB", (w, h), (255, 255, 255)), + Image.new("RGB", (w, h), (0, 0, 0)), + hl + ), + scale=1.0 + ) + union_alpha = ImageChops.lighter(union_alpha, hl) + + # Convert black background to MAGENTA where union_alpha == 0 + mag = Image.new("RGB", (w, h), c3.MAGENTA) + comp_rgb = comp + # mask where alpha==0 => choose magenta else comp + bgmask = union_alpha.point(lambda v: 255 if v == 0 else 0, mode="L") + final_rgb = Image.composite(mag, comp_rgb, bgmask) # bgmask selects magenta where 255 + + # Make RGBA (opaque; transparency will be via palette index 255 later) + return final_rgb.convert("RGBA") + + +def quantize_overlay_to_civ3_p( + overlay_rgba: Image.Image, + transparent_index: int = 255, + fixed_palette: Optional[Image.Image] = None, +) -> Image.Image: + """ + Quantize RGBA overlay to P/256 and enforce Civ3 palette rules: + - MAGENTA at palette index 255 + - any magenta pixels -> index 255 + - remove GREEN if present in palette (like your compositor does) + """ + # Use a single shared palette for all frames when provided. + if fixed_palette is None: + imP = c3.quantize_to_p_256(overlay_rgba) + else: + try: + dnone = Image.Dither.NONE + except Exception: + dnone = getattr(Image, "NONE", 0) + imP = overlay_rgba.convert("RGB").quantize(palette=fixed_palette, dither=dnone) + + # Ensure MAGENTA exists, then force it to 255 + pal = c3.ensure_palette_has_colors(imP, [c3.MAGENTA]) + c3.put_palette(imP, pal) + replacement_idx = c3.force_magenta_at_255(imP) + + # Force magenta pixels to index 255, and avoid leaving replacement_idx as 255. + px = imP.load() + w, h = imP.size + # Use source RGB to detect magenta precisely + src_rgb = overlay_rgba.convert("RGB").load() + for y in range(h): + for x in range(w): + r, g, b = src_rgb[x, y] + cur = px[x, y] + if (r, g, b) == c3.MAGENTA: + px[x, y] = transparent_index + elif cur == transparent_index and replacement_idx != transparent_index: + # if quantizer mapped something to 255 that is not magenta, remap + px[x, y] = replacement_idx + + # Remove GREEN from palette (optional; matches your compositor behavior) + pal_after = c3.get_palette(imP) + for i in range(256): + if (pal_after[3*i], pal_after[3*i+1], pal_after[3*i+2]) == c3.GREEN: + pal_after[3*i:3*i+3] = [0, 0, 0] + c3.put_palette(imP, pal_after) + + return imP + + +def count_light_key_pixels( + mask_source_rgb: Image.Image, + keys: List[Tuple[int, int, int]], + styles: Dict[Tuple[int, int, int], Dict[str, object]], +) -> Dict[Tuple[int, int, int], int]: + counts: Dict[Tuple[int, int, int], int] = {} + keys_set = set(keys) + keys_set.update(styles.keys()) + for key_rgb in keys_set: + m = c3.color_equal(mask_source_rgb, key_rgb) + counts[key_rgb] = int(m.histogram()[255]) + return counts + + +def parse_light_styles_local(values: List[str]) -> Dict[Tuple[int, int, int], Dict[str, object]]: + """ + Local style parser for flicker generation. + Keeps compatibility with civ3_city_lights style syntax, and also accepts: + - highlight_gain (alias for highlight) + - blend_mode (per-key: add|screen) + """ + styles: Dict[Tuple[int, int, int], Dict[str, object]] = {} + for raw in values or []: + parts = [p.strip() for p in raw.replace(",", ";").split(";") if p.strip()] + kv: Dict[str, str] = {} + for p in parts: + if "=" in p: + k, v = p.split("=", 1) + kv[k.strip().lower()] = v.strip() + if "key" not in kv: + raise SystemExit("Each --light-style must include key=") + + key_rgb = c3.parse_rgb_one(kv["key"]) + entry: Dict[str, object] = {} + + if "core" in kv: + entry["core_color"] = c3.parse_rgb_one(kv["core"]) + if "glow" in kv: + entry["glow_color"] = c3.parse_rgb_one(kv["glow"]) + + for numk in [ + "core_gain", "halo_gain", "core_radius", "halo_radius", + "halo_sep", "halo_gamma", "highlight", "size_boost", + "size_radius", "size_gamma" + ]: + if numk in kv: + entry[numk] = float(kv[numk]) + + if "highlight_gain" in kv: + entry["highlight"] = float(kv["highlight_gain"]) + + if "blend_mode" in kv: + bm = kv["blend_mode"].strip().lower() + if bm in ("screen", "add"): + entry["blend_mode"] = bm + + styles[key_rgb] = entry + return styles + + +def build_shared_palette_source(overlays_rgba: List[Image.Image]) -> Image.Image: + if not overlays_rgba: + raise ValueError("No overlays to build shared palette") + # Build palette from all frame pixels (not a max-light merge), so darker shades remain available. + w, h = overlays_rgba[0].size + atlas = Image.new("RGB", (w, h * len(overlays_rgba))) + for i, ov in enumerate(overlays_rgba): + atlas.paste(ov.convert("RGB"), (0, i * h)) + return c3.quantize_to_p_256(atlas) + + +# ----------------------------- +# Sheet slicing +# ----------------------------- +def get_cell_box(sheet_w: int, sheet_h: int, rows: int, cols: int, r: int, c: int, + cell_w: int, cell_h: int) -> Tuple[int, int, int, int]: + left = c * cell_w + top = r * cell_h + return (left, top, left + cell_w, top + cell_h) + + +# ----------------------------- +# Main +# ----------------------------- +def main() -> None: + ap = argparse.ArgumentParser( + description="Generate Civ3 overlay FLC flicker animations from a Civ3-style *_lights.pcx annotation sheet." + ) + ap.add_argument("--in", dest="inp", required=True, help="Input annotation PCX sheet (lights markers).") + ap.add_argument("--out-dir", required=True, help="Output directory for per-cell .flc files.") + + ap.add_argument("--rows", type=int, required=True, help="Rows (eras).") + ap.add_argument("--cols", type=int, required=True, help="Cols (variants).") + + ap.add_argument("--cell-w", type=int, default=128, help="Cell width (default 128).") + ap.add_argument("--cell-h", type=int, default=64, help="Cell height (default 64).") + + ap.add_argument("--frames", type=int, default=12, help="Animation frames (excluding keyframe/ring).") + ap.add_argument("--fps", type=float, default=12.0, help="FPS used for Civ3 anim_time metadata.") + + ap.add_argument("--transparent-index", type=int, default=255, help="Transparent palette index (default 255).") + + # Flicker tuning + ap.add_argument("--amp", type=float, default=0.12, help="Flicker amplitude (0.05..0.25 typical).") + ap.add_argument("--hz1", type=float, default=2.2, help="Primary flicker frequency (Hz).") + ap.add_argument("--hz2", type=float, default=5.1, help="Secondary flicker frequency (Hz).") + ap.add_argument("--seed", type=int, default=1337, help="Deterministic seed.") + ap.add_argument("--frame-change-rate", type=float, default=0.75, + help="0..1. Lower = more gradual frame-to-frame change, higher = snappier.") + + # Light keys/styles (same syntax as your compositor) + ap.add_argument("--light-key", action="append", default=["#00feff"], + help="Marker color(s) in the annotation PCX, repeatable. '#rrggbb' or 'R,G,B'.") + ap.add_argument("--light-style", action="append", default=[], + help="Per-key overrides. Example: \"key=#00feff; core=#fff87a; glow=#ff8a20; halo_gain=18; halo_radius=14\"") + + # Global glow defaults (matching civ3_city_lights) + ap.add_argument("--core-radius", type=float, default=1.1) + ap.add_argument("--halo-radius", type=float, default=13.0) + ap.add_argument("--core-gain", type=float, default=2.1) + ap.add_argument("--halo-gain", type=float, default=20.0) + ap.add_argument("--core-color", type=str, default="#ff8a20") + ap.add_argument("--glow-color", type=str, default="#dc6a00") + ap.add_argument("--highlight-gain", type=float, default=0.6) + + ap.add_argument("--size-boost", type=float, default=1.1) + ap.add_argument("--size-radius", type=float, default=3.5) + ap.add_argument("--size-gamma", type=float, default=0.75) + + ap.add_argument("--halo-sep", type=float, default=0.75) + ap.add_argument("--halo-gamma", type=float, default=1.4) + ap.add_argument("--blend-mode", type=str, default="screen", choices=["screen", "add"]) + + # Civ3 header knobs + ap.add_argument("--civ-num-anims", type=int, default=1, help="Directions count in Civ3 header (usually 1).") + ap.add_argument("--civ-directions-mask", type=lambda s: int(s, 0), default=0x0001, + help="Directions bitmask in Civ3 header (default 0x0001).") + ap.add_argument("--civ-xs-orig", type=int, default=240, help="xs_orig in Civ3 FlicAnimHeader (default 240).") + ap.add_argument("--civ-ys-orig", type=int, default=240, help="ys_orig in Civ3 FlicAnimHeader (default 240).") + ap.add_argument("--with-ring-frame", action="store_true", help="Append explicit ring frame (off by default).") + ap.add_argument("--flc-speed", type=int, default=170, + help="FLC header speed/delay in milliseconds for viewer playback (default 170).") + + ap.add_argument("--name-prefix", default="Lights", help="Output naming prefix.") + + args = ap.parse_args() + args.frame_change_rate = max(0.0, min(1.0, args.frame_change_rate)) + + os.makedirs(args.out_dir, exist_ok=True) + + sheet = Image.open(args.inp) + # mask source must be RGB to compare marker colors exactly + sheet_rgb = sheet.convert("RGB") + + sw, sh = sheet.size + expected_w = args.cols * args.cell_w + expected_h = args.rows * args.cell_h + if sw < expected_w or sh < expected_h: + raise SystemExit(f"Sheet is {sw}x{sh}, but rows*cell is {expected_w}x{expected_h}. Check --rows/--cols/--cell-w/--cell-h.") + + # Parse keys with compositor parser; parse styles locally to support + # flicker-specific compatibility aliases like highlight_gain. + light_keys = c3.parse_rgb_list(args.light_key) + styles = parse_light_styles_local(args.light_style) + core_color = c3.parse_rgb_one(args.core_color) + glow_color = c3.parse_rgb_one(args.glow_color) + + for r in range(args.rows): + for c in range(args.cols): + box = get_cell_box(sw, sh, args.rows, args.cols, r, c, args.cell_w, args.cell_h) + cell_mask_rgb = sheet_rgb.crop(box) + + key_counts = count_light_key_pixels(cell_mask_rgb, light_keys, styles) + total_key_pixels = sum(key_counts.values()) + if total_key_pixels == 0: + print( + f"WARNING: Cell r{r:02d}c{c:02d} has 0 marker pixels for keys " + f"{[f'#{k[0]:02x}{k[1]:02x}{k[2]:02x}' for k in key_counts.keys()]}; " + "output will be fully transparent (magenta in editor previews)." + ) + + # Build frame overlays first, then quantize all frames with a shared palette. + cell_phase = _phase_for_cell(r, c, args.seed) + overlays_rgba: List[Image.Image] = [] + prev_overlay: Optional[Image.Image] = None + for i in range(args.frames): + mult = _flicker_multiplier(i, args.frames, args.fps, cell_phase, args.amp, args.hz1, args.hz2) + + overlay_rgba = render_overlay_frame_rgba( + mask_source_rgb=cell_mask_rgb, + keys=light_keys, + styles=styles, + intensity=mult, + core_radius=args.core_radius, + halo_radius=args.halo_radius, + core_gain=args.core_gain, + halo_gain=args.halo_gain, + core_color=core_color, + glow_color=glow_color, + size_boost=args.size_boost, + size_radius=args.size_radius, + size_gamma=args.size_gamma, + highlight_gain=args.highlight_gain, + blend_mode=args.blend_mode, + halo_sep=args.halo_sep, + halo_gamma=args.halo_gamma, + ) + + # Add pixel-level shimmer variation. + overlay_rgba = apply_per_pixel_flicker( + overlay_rgba=overlay_rgba, + frame_i=i, + fps=args.fps, + amp=args.amp * 1.25, + hz1=args.hz1, + hz2=args.hz2, + seed=args.seed ^ (r * 10007 + c * 10009), + ) + overlay_rgba = apply_global_flicker_gain( + overlay_rgba=overlay_rgba, + weight_0_1=mult, + strength=1.0, + ) + + # Smooth temporal changes: lower rate => more gradual transitions. + if prev_overlay is not None and args.frame_change_rate < 1.0: + overlay_rgba = Image.blend(prev_overlay, overlay_rgba, args.frame_change_rate) + prev_overlay = overlay_rgba + overlays_rgba.append(overlay_rgba) + + palette_source = build_shared_palette_source(overlays_rgba) + frames_p = [ + quantize_overlay_to_civ3_p( + overlay_rgba, + transparent_index=args.transparent_index, + fixed_palette=palette_source, + ) + for overlay_rgba in overlays_rgba + ] + adjusted = ensure_adjacent_frames_differ(frames_p, args.transparent_index) + if adjusted > 0: + print(f"NOTE: Cell r{r:02d}c{c:02d} had {adjusted} quantized frame collapse(s); applied anti-collapse nudges.") + + out_name = f"{args.name_prefix}_r{r:02d}c{c:02d}.flc" + out_path = os.path.join(args.out_dir, out_name) + + write_flc( + out_path=out_path, + frames_p=frames_p, + fps=args.fps, + civ_num_anims=args.civ_num_anims, + civ_directions_bitmask=args.civ_directions_mask, + civ_x_offset=0, + civ_y_offset=0, + civ_xs_orig=args.civ_xs_orig, + civ_ys_orig=args.civ_ys_orig, + include_ring_frame=args.with_ring_frame, + flc_speed_ms=args.flc_speed, + ) + + ring_note = "+ring" if args.with_ring_frame else "" + print(f"Wrote {out_path} ({args.cell_w}x{args.cell_h}, frames={args.frames}{ring_note})") + + +if __name__ == "__main__": + main() diff --git a/DayNight/rgb_to_civ3_indexed_pcx.py b/DayNight/rgb_to_civ3_indexed_pcx.py new file mode 100644 index 00000000..8597c3d2 --- /dev/null +++ b/DayNight/rgb_to_civ3_indexed_pcx.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +pcx_rgb_to_civ3_indexed_pcx.py + +Input: PCX file (intended to be RGB / truecolor) +Output: PCX file (indexed / paletted) with Civ3 palette rules: + +Palette indices (256 entries): + 0..63 reserved civ colors (set to white) + 64..253 sampled from the image (190 colors) + 254 green (#00ff00) + 255 magenta(#ff00ff) + +Dependencies: + pip install pillow numpy + +Usage: + python pcx_rgb_to_civ3_indexed_pcx.py --in input_rgb.pcx --out output_indexed.pcx +""" + +from __future__ import annotations + +import argparse +import math +from typing import List, Tuple + +import numpy as np +from PIL import Image + +MAGENTA = (255, 0, 255) +GREEN = (0, 255, 0) +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) + +RESERVED_CIV_COLORS = 64 +RESERVED_TAIL = 2 +TOTAL_PALETTE = 256 +SAMPLED_COLORS = TOTAL_PALETTE - RESERVED_CIV_COLORS - RESERVED_TAIL # 190 + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser() + p.add_argument("--in", dest="inp", required=True, help="Input PCX path (RGB PCX)") + p.add_argument("--out", required=True, help="Output PCX path (indexed Civ3 style)") + p.add_argument( + "--sample_thumb", + type=int, + default=256, + help="Max thumb dimension used for palette sampling (default 256)", + ) + p.add_argument( + "--dither", + action="store_true", + help="Enable dithering (usually OFF for Civ3 assets)", + ) + p.add_argument( + "--snap_reserved_tol", + type=int, + default=0, + help=( + "If >0, snap pixels within this per-channel tolerance to exact reserved colors " + "(helps if editing introduced near-magenta/near-green)." + ), + ) + return p.parse_args() + + +def is_pcx_path(path: str) -> bool: + return path.lower().endswith(".pcx") + + +def snap_reserved_colors(arr: np.ndarray, tol: int) -> np.ndarray: + """ + Snap near-magenta and near-green pixels back to exact MAGENTA / GREEN. + tol is per-channel tolerance (0 disables). + """ + if tol <= 0: + return arr + + r = arr[:, :, 0].astype(np.int16) + g = arr[:, :, 1].astype(np.int16) + b = arr[:, :, 2].astype(np.int16) + + def near(color: Tuple[int, int, int]) -> np.ndarray: + cr, cg, cb = color + return ( + (np.abs(r - cr) <= tol) + & (np.abs(g - cg) <= tol) + & (np.abs(b - cb) <= tol) + ) + + near_magenta = near(MAGENTA) + near_green = near(GREEN) + + arr[near_magenta] = MAGENTA + arr[near_green] = GREEN + return arr + + +def build_sample_palette(im_rgb: Image.Image, thumb_max: int) -> List[Tuple[int, int, int]]: + """ + Build up to 190 representative colors from the image safely. + """ + + w, h = im_rgb.size + scale = max(1, math.ceil(max(w, h) / max(1, int(thumb_max)))) + tw, th = max(1, w // scale), max(1, h // scale) + thumb = im_rgb.resize((tw, th), resample=Image.NEAREST) + + arr = np.array(thumb, dtype=np.uint8) + + # Remove reserved colors from sampling influence + is_magenta = (arr[:, :, 0] == 255) & (arr[:, :, 1] == 0) & (arr[:, :, 2] == 255) + is_green = (arr[:, :, 0] == 0) & (arr[:, :, 1] == 255) & (arr[:, :, 2] == 0) + arr[is_magenta | is_green] = BLACK + + thumb2 = Image.fromarray(arr, mode="RGB") + + q = thumb2.quantize(colors=SAMPLED_COLORS, method=Image.MEDIANCUT, dither=Image.NONE) + + pal = q.getpalette() + if pal is None: + return [] + + # Determine how many palette entries actually exist + actual_entries = len(pal) // 3 + + colors = [] + for i in range(min(actual_entries, SAMPLED_COLORS)): + base = i * 3 + r, g, b = pal[base], pal[base + 1], pal[base + 2] + colors.append((r, g, b)) + + # Remove reserved colors + dedupe + seen = set() + uniq = [] + for c in colors: + if c in (MAGENTA, GREEN): + continue + if c not in seen: + uniq.append(c) + seen.add(c) + + return uniq + + +def make_civ3_palette(sampled: List[Tuple[int, int, int]]) -> List[int]: + """ + Produce a 256*3 palette list. + """ + pal: List[int] = [] + + # 0..63 = white placeholders + for _ in range(RESERVED_CIV_COLORS): + pal += [*WHITE] + + sampled = [c for c in sampled if c not in (GREEN, MAGENTA)] + sampled = sampled[:SAMPLED_COLORS] + if len(sampled) < SAMPLED_COLORS: + sampled = sampled + [BLACK] * (SAMPLED_COLORS - len(sampled)) + + for (r, g, b) in sampled: + pal += [r, g, b] + + # 254 green, 255 magenta + pal += [*GREEN] + pal += [*MAGENTA] + + assert len(pal) == 768 + return pal + + +def main() -> None: + args = parse_args() + + if not is_pcx_path(args.inp): + raise SystemExit("Input must be a .pcx file") + if not is_pcx_path(args.out): + raise SystemExit("Output must be a .pcx file") + + # Read PCX + im = Image.open(args.inp) # may fail if PCX is saved in an odd truecolor variant + im_rgb = im.convert("RGB") + + # Optional snapping of near-reserved colors back to exact reserved values + arr = np.array(im_rgb, dtype=np.uint8) + arr = snap_reserved_colors(arr, tol=int(args.snap_reserved_tol)) + im_rgb = Image.fromarray(arr, mode="RGB") + + # Build fixed Civ3 palette and quantize to it + sampled = build_sample_palette(im_rgb, thumb_max=int(args.sample_thumb)) + palette_list = make_civ3_palette(sampled) + + pal_img = Image.new("P", (16, 16)) + pal_img.putpalette(palette_list) + + dither_flag = Image.FLOYDSTEINBERG if args.dither else Image.NONE + indexed = im_rgb.quantize(palette=pal_img, dither=dither_flag) + + # Save indexed PCX + indexed.save(args.out, format="PCX") + print(f"Saved indexed Civ3 PCX: {args.out}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/DayNight/test_light_flc.sh b/DayNight/test_light_flc.sh new file mode 100644 index 00000000..fafd1a4c --- /dev/null +++ b/DayNight/test_light_flc.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./test_light_flx.sh --in --out-dir [options] + +Required: + --in PATH Input *_lights.pcx annotation sheet + --out-dir PATH Output directory for generated .flc files + +Options: + --rows N Number of rows in sheet (default: 1) + --cols N Number of columns in sheet (default: 1) + --cell-w N Cell width (default: 128) + --cell-h N Cell height (default: 64) + --frames N Animation frames (default: 12) + --fps N FPS metadata value (default: 12) + --frame-change-rate N 0..1 temporal smoothing (default: 1.0 for clear motion) + --hz1 N Primary flicker frequency in Hz (default: 1.2) + --hz2 N Secondary flicker frequency in Hz (default: 2.4) + --name-prefix NAME Output name prefix (default: Lights) + --with-ring-frame Append ring frame + --python CMD Python executable (default: python3) + --help Show this help + +Any unrecognized args are passed through to pcx_sheet_to_civ3_flc_flicker.py. +EOF +} + +INP="" +OUT_DIR="" +ROWS=1 +COLS=1 +CELL_W=128 +CELL_H=64 +FRAMES=12 +FPS=12 +FRAME_CHANGE_RATE=1.0 +HZ1=1.2 +HZ2=2.4 +NAME_PREFIX="Lights" +WITH_RING_FRAME=0 +PYTHON_BIN="python" + +EXTRA_ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --in) INP="${2:-}"; shift 2 ;; + --out-dir) OUT_DIR="${2:-}"; shift 2 ;; + --rows) ROWS="${2:-}"; shift 2 ;; + --cols) COLS="${2:-}"; shift 2 ;; + --cell-w) CELL_W="${2:-}"; shift 2 ;; + --cell-h) CELL_H="${2:-}"; shift 2 ;; + --frames) FRAMES="${2:-}"; shift 2 ;; + --fps) FPS="${2:-}"; shift 2 ;; + --frame-change-rate) FRAME_CHANGE_RATE="${2:-}"; shift 2 ;; + --hz1) HZ1="${2:-}"; shift 2 ;; + --hz2) HZ2="${2:-}"; shift 2 ;; + --name-prefix) NAME_PREFIX="${2:-}"; shift 2 ;; + --with-ring-frame) WITH_RING_FRAME=1; shift ;; + --python) PYTHON_BIN="${2:-}"; shift 2 ;; + --help|-h) usage; exit 0 ;; + *) EXTRA_ARGS+=("$1"); shift ;; + esac +done + +if [[ -z "$INP" || -z "$OUT_DIR" ]]; then + usage + exit 1 +fi + +if [[ ! -f "$INP" ]]; then + echo "Input file not found: $INP" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FLICKER_SCRIPT="$SCRIPT_DIR/pcx_sheet_to_civ3_flc_flicker.py" + +if [[ ! -f "$FLICKER_SCRIPT" ]]; then + echo "Missing script: $FLICKER_SCRIPT" >&2 + exit 1 +fi + +# Match generate_light_pcx.sh key configuration. +LIGHT_KEYS=( + "#F6915E" # Orange + "#FEF500" # Yellow + "#00feff" # Teal + "#E4080A" # Red + "#BD15D0" # Purple + "#2D9C01" # Green + "#FF25C8" # Pink + "#0A02EB" # Blue + "#8262ED" # Indigo +) + +LIGHT_STYLES=( + "key=#F6915E; core=#ff8a20; glow=#dc6a00; core_gain=1.0; highlight_gain=0.0; size_radius=1.5; size_boost=0.05; halo_gain=6.0; halo_radius=1.0; core_radius=0.5; halo_gamma=1.5; size_gamma=0.1;" + "key=#FEF500; core=#ff8a20; glow=#dc6a00; core_gain=2.5; highlight_gain=1.0; size_radius=6.5; size_boost=1.5; halo_gain=20.0; halo_radius=0.1; core_radius=1.1; halo_gamma=1.3; size_gamma=0.75;" + "key=#E4080A; core=#E4080A; glow=#E4080A; core_gain=1.0; highlight_gain=0.0; size_radius=0.5; size_boost=0.0; halo_gain=6.0; halo_radius=1.0; core_radius=0.5; halo_gamma=0.9; size_gamma=0.0; blend_mode=add;" + "key=#00feff; core=#00feff; glow=#00feff; core_gain=0.6; highlight_gain=0.0; size_radius=0.9; size_boost=0.3; halo_gain=8.0; halo_radius=0.1; core_radius=0.5; halo_gamma=1.5; size_gamma=0.1;" + "key=#BD15D0; core=#BD15D0; glow=#BD15D0; core_gain=0.6; highlight_gain=0.0; size_radius=0.9; size_boost=0.3; halo_gain=8.0; halo_radius=0.1; core_radius=0.5; halo_gamma=1.5; size_gamma=0.1;" + "key=#2D9C01; core=#2D9C01; glow=#2D9C01; core_gain=0.6; highlight_gain=0.0; size_radius=0.9; size_boost=0.3; halo_gain=8.0; halo_radius=0.1; core_radius=0.5; halo_gamma=1.5; size_gamma=0.1;" + "key=#FF25C8; core=#FF25C8; glow=#FF25C8; core_gain=0.6; highlight_gain=0.0; size_radius=0.9; size_boost=0.3; halo_gain=8.0; halo_radius=0.1; core_radius=0.5; halo_gamma=1.5; size_gamma=0.1;" + "key=#0A02EB; core=#0A02EB; glow=#0A02EB; core_gain=0.2; highlight_gain=0.0; size_radius=0.9; size_boost=0.3; halo_gain=8.0; halo_radius=0.1; core_radius=0.5; halo_gamma=1.5; size_gamma=0.1;" + "key=#8262ED; core=#7521DC; glow=#7521DC; core_gain=0.2; highlight_gain=0.0; size_radius=0.9; size_boost=0.3; halo_gain=8.0; halo_radius=0.1; core_radius=0.5; halo_gamma=1.5; size_gamma=0.1;" +) + +LK_ARGS=() +for c in "${LIGHT_KEYS[@]}"; do + LK_ARGS+=(--light-key "$c") +done + +STYLE_ARGS=() +for s in "${LIGHT_STYLES[@]}"; do + STYLE_ARGS+=(--light-style "$s") +done + +# Match generate_light_pcx.sh global city-light defaults. +GLOBAL_ARGS=( + --core-color "#ff8a20" + --glow-color "#dc6a00" + --core-radius 1.1 + --halo-radius 13.0 + --core-gain 2.5 + --halo-gain 20.0 + --highlight-gain 0.5 + --size-boost 1.7 + --size-radius 6.5 + --size-gamma 0.75 + --halo-sep 0.75 + --halo-gamma 1.3 + --blend-mode screen +) + +CMD=( + "$PYTHON_BIN" "$FLICKER_SCRIPT" + --in "$INP" + --out-dir "$OUT_DIR" + --rows "$ROWS" + --cols "$COLS" + --cell-w "$CELL_W" + --cell-h "$CELL_H" + --frames "$FRAMES" + --fps "$FPS" + --frame-change-rate "$FRAME_CHANGE_RATE" + --hz1 "$HZ1" + --hz2 "$HZ2" + --name-prefix "$NAME_PREFIX" + "${GLOBAL_ARGS[@]}" + "${LK_ARGS[@]}" + "${STYLE_ARGS[@]}" + "${EXTRA_ARGS[@]}" +) + +if [[ "$WITH_RING_FRAME" -eq 1 ]]; then + CMD+=(--with-ring-frame) +fi + +echo "Running:" +printf ' %q' "${CMD[@]}" +echo +"${CMD[@]}" diff --git a/Trade Net X/Civ3Conquests.hpp b/Trade Net X/Civ3Conquests.hpp index 095398c3..579a1777 100644 --- a/Trade Net X/Civ3Conquests.hpp +++ b/Trade Net X/Civ3Conquests.hpp @@ -206,7 +206,7 @@ typedef struct Unit_Body Unit_Body; typedef struct Tile Tile; typedef struct Map Map; typedef struct BIC BIC; -typedef struct _1A4 _1A4; +typedef struct Tile_Animated_Effect Tile_Animated_Effect; typedef struct Scroll_Bar Scroll_Bar; typedef struct Base_Form Base_Form; typedef struct Advisor_Military_Form Advisor_Military_Form; @@ -1557,7 +1557,7 @@ struct UnitType int field_CC; int b_Not_King; int field_D4; - int field_D8; + int active_tile_effect; IntList unit_telepads; int field_F4; IntList stealth_attack_targets; @@ -3795,7 +3795,7 @@ struct Animation_Info Flic_Anim_Info **Animations; int field_140[17]; int field_184[21]; - float *field_1D8; + float *anim_frame_time_seconds; int field_1DC; int field_1E0; int field_1E4; @@ -4273,7 +4273,7 @@ struct Tile_Body short field_92; int field_D0_Visibility; int field_D4; - _1A4 *field_D8; + Tile_Animated_Effect *active_tile_effect; }; struct Race @@ -4346,7 +4346,7 @@ struct City_Body Population Population; int CultureIncome; int Total_Cultures[32]; - int field_1A4; + int fieldTile_Animated_Effect; int Rioting_Change_Value; int Tiles_Food; int Tiles_Production; @@ -4355,7 +4355,7 @@ struct City_Body int rally_point_x; int rally_point_y; char CityName[20]; - int field_1D8; + int anim_frame_time_seconds; int Order_Queue_Count; City_Order Orders_Queue[9]; int FoodRequired; @@ -5016,10 +5016,10 @@ struct BIC Map Map; }; -struct _1A4 +struct Tile_Animated_Effect { int V[3]; - FLC_Animation struct_188; + FLC_Animation flc_animation; int field_194[4]; }; @@ -5832,7 +5832,7 @@ struct City_Form int Units_Start_Index; int field_96B0; int field_96B4[5]; - FLC_Animation struct_188; + FLC_Animation flc_animation; City_Form_Labels Labels; int field_98D0[67]; Tile_Image_Info QueueBase_Image; diff --git a/civ_prog_objects.csv b/civ_prog_objects.csv index 4d819f7e..3ef2bbc5 100644 --- a/civ_prog_objects.csv +++ b/civ_prog_objects.csv @@ -967,6 +967,10 @@ define, 0xCC2BB0, 0xCE54BC, 0xCC2B70, "p_got_leader_gender", "int *" ignore, 0x49D070, 0x4A3AF0, 0x49D100, "Advisor_GUI_open", "void (__fastcall *) (Advisor_GUI * this, int edx, AdvisorKind kind)" ignore, 0x4BF660, 0x4C6C10, 0x4BF6F0, "City_draw_citizens", "void (__fastcall *) (City * this, int edx, PCX_Image * canvas, RECT * rect, char param_3)" ignore, 0x4B9F60, 0x4C15D0, 0x4B9FF0, "City_add_population", "void (__fastcall *) (City * this, int edx, int num, int race_id)" -ignore, 0x670234, 0x68D2E0, 0x670234, "Tile_m27_Check_Shield_Bonus", "bool (__fastcall *) (Tile * this)" -ignore, 0x5f3448, 0x6032DF, 0x5F3378, "CHECK_SHIELD_BONUS_TO_CAN_SPAWN_RES_RETURN", "int" - +ignore, 0x670234, 0x68D2E0, 0x670234, "Tile_m27_Check_Shield_Bonus", "bool (__fastcall *) (Tile * this)" +ignore, 0x5f3448, 0x6032DF, 0x5F3378, "CHECK_SHIELD_BONUS_TO_CAN_SPAWN_RES_RETURN", "int" +inlead, 0x4DE5C0, 0x0, 0x0, "on_timer_0x9F6500", "void (__stdcall *) (void)" +define, 0x4062A0, 0x0, 0x0, "Units_Image_Data_load_animation", "void (__fastcall *) (Units_Image_Data * this, int edx, char * asset_string, FLC_Animation * anim, int civ_id, int param_4, int param_5, bool param_6)" +inlead, 0x4069E0, 0x0, 0x0, "Units_Image_Data_load_animated_effect", "void (__fastcall *) (Units_Image_Data * this, int edx, FLC_Animation * anim, int effect_id)" +inlead, 0x5DA5E0, 0x0, 0x0, "Tile_spawn_animated_effect", "void (__fastcall *) (Tile * this, int edx, enum AnimatedEffect effect, int tile_x, int tile_y, bool randomize_start_frame, enum direction dummy_dir)" +define, 0x5DA7A0, 0x0, 0x0, "Tile_clear_animated_effect", "void * (__fastcall *) (Tile * this)" diff --git a/common.c b/common.c index 7c39733f..852cc77d 100644 --- a/common.c +++ b/common.c @@ -592,6 +592,87 @@ read_int (struct string_slice const * s, int * out_val) return 0; } +int +read_float (struct string_slice const * s, float * out_val) +{ + struct string_slice trimmed = trim_string_slice (s, 1); + if (trimmed.len <= 0) + return 0; + + char * str = extract_slice (&trimmed); + if (str == NULL) + return 0; + + char * cur = str; + int sign = 1; + if (*cur == '-') { + sign = -1; + cur++; + } else if (*cur == '+') + cur++; + + double parsed = 0.0; + int saw_digit = 0; + while ((*cur >= '0') && (*cur <= '9')) { + saw_digit = 1; + parsed = parsed * 10.0 + (double)(*cur - '0'); + cur++; + } + + if (*cur == '.') { + double place = 0.1; + cur++; + while ((*cur >= '0') && (*cur <= '9')) { + saw_digit = 1; + parsed += (double)(*cur - '0') * place; + place *= 0.1; + cur++; + } + } + + if (! saw_digit) { + free (str); + return 0; + } + + if ((*cur == 'e') || (*cur == 'E')) { + cur++; + int exp_sign = 1; + if (*cur == '-') { + exp_sign = -1; + cur++; + } else if (*cur == '+') + cur++; + + int exp = 0; + int saw_exp_digit = 0; + while ((*cur >= '0') && (*cur <= '9')) { + saw_exp_digit = 1; + exp = exp * 10 + (*cur - '0'); + cur++; + } + if (! saw_exp_digit) { + free (str); + return 0; + } + + while (exp > 0) { + if (exp_sign > 0) + parsed *= 10.0; + else + parsed *= 0.1; + exp--; + } + } + + skip_horiz_space (&cur); + int ok = (*cur == '\0'); + if (ok) + *out_val = (float)(sign * parsed); + free (str); + return ok; +} + int read_i31b (struct string_slice const * s, i31b * out_i31b_val) { @@ -668,6 +749,18 @@ parse_int (char ** p_cursor, int * out) return 0; } +int +parse_float (char ** p_cursor, float * out) +{ + char * cur = *p_cursor; + struct string_slice ss; + if (parse_string (&cur, &ss) && read_float (&ss, out)) { + *p_cursor = cur; + return 1; + } else + return 0; +} + int parse_i31b (char ** p_cursor, int * out_i31b_val) { diff --git a/default.c3x_config.ini b/default.c3x_config.ini index e489d2f3..ed07d16b 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -931,6 +931,16 @@ aircraft_victory_animation = none ; Enables naming tiles via the right-click menu and displays those names on the map. enable_named_tiles = true +; Enables custom tile FLC animations for terrain types and resources. +enable_custom_animations = false + +; A list of unit actions that will show the tile destruction animation when destroyed. Valid names are: bombard, bomb, pillage. +show_tile_destruct_animation_after = [ bombard bomb pillage ] + +; The number of turns to show the tile destruction animation after a tile is destroyed by one of the actions above. +; For example, if set to 2, the animation will be shown for the remainder of the turn in which the tile was destroyed and the next turn, but not after that. +show_tile_destruction_animation_for_turns = 2 + [=======================] [=== DAY/NIGHT CYCLE ===] [=======================] @@ -957,6 +967,40 @@ fixed_hours_per_turn_for_day_night_cycle = 1 ; If day_night_cycle_mode is set to 'specified', the hour of the day to pin the cycle to. This should be a value between 0 (midnight) and 23 (11pm) inclusive. pinned_hour_for_day_night_cycle = 0 +[======================] +[=== SEASONAL CYCLE ===] +[======================] + +; How the seasonal cycle operates in the game. If enabled, terrain/city/resource/district art can vary by season. +; Seasonal art is loaded from Art/DayNight//... and Art/Districts//... +; The possible values are: +; off: Only summer art used +; timer: Transition based on elapsed real-time minutes +; user-season: Match the user's local month to season (Winter=Dec-Feb, Spring=Mar-May, Summer=Jun-Aug, Fall=Sep-Nov) +; every-turn: Transition every fixed number of turns +; on-day-night-hour: Transition when the day/night cycle reaches a configured hour +; specified: Pin to one season +seasonal_cycle_mode = off + +; Comma-delimited list of enabled seasons. Valid names: summer, fall (alt: autumn), winter, spring +; Remove seasons from this list to prevent them from appearing in the cycle. +enabled_seasons = [ summer fall winter spring ] + +; If seasonal_cycle_mode is set to 'specified', pin to this season. +; Accepts season name (summer/fall/winter/spring) +pinned_season_for_seasonal_cycle = summer + +; If seasonal_cycle_mode is set to 'timer', minimum minutes of real time to elapse before season transition. +; This is checked only at end of turn, so actual minutes may exceed this number. +elapsed_minutes_per_season_transition = 3 + +; If seasonal_cycle_mode is set to 'every-turn', number of turns per season before transitioning to next enabled season. +fixed_turns_per_season = 3 + +; If seasonal_cycle_mode is set to 'on-day-night-hour', transition to next enabled season when the +; day/night cycle reaches this hour. Valid range is 0 (midnight) to 23 (11pm). +transition_season_on_day_night_hour = 0 + [=======================] [=== NATURAL WONDERS ===] [=======================] diff --git a/default.districts_config.txt b/default.districts_config.txt index 02f3486f..8137ada5 100644 --- a/default.districts_config.txt +++ b/default.districts_config.txt @@ -54,6 +54,13 @@ ; - custom_height : Number (pixels). Override sprite height. (default: 64) ; - x_offset : Number (pixels). Push the sprite farther to the right (or left, if negative). (default: 0) ; - y_offset : Number (pixels). Push the sprite farther down (or up, if negative). (default: 0) + ; - animation : Custom FLC animation shown over completed districts. May be repeated; the most specific matching entry is chosen. + ; Syntax: ini=; hours=<0..23 list>; seasons=; cultures=; eras=; frame_time_seconds=; offsets= + ; ini is under Art/Animations. Optional hours accepts values/ranges like 7-17 or 18-5. + ; Optional seasons: spring, summer, fall, winter. Optional cultures: AMER, EURO, ROMAN, MIDEAST, ASIAN. + ; Optional eras: ancient, middle, industrial, modern, or 0..3. Optional frame_time_seconds controls playback speed. + ; Optional offsets shift the animation in pixels. + ; Example: animation = ini=Districts\Smoke.INI; hours=7-17; seasons=spring,summer; cultures=AMER,EURO; eras=industrial,modern; frame_time_seconds=0.12; offsets=0,-10 ; - align_to_coast : 0 or 1. Aligns art to coastline, slightly adjusting x & y pixels. (default: 0) ; - auto_add_road : 0 or 1. Auto-add road on completion. (default: 0) ; - auto_add_railroad : 0 or 1. Auto-add railroad on completion. (default: 0) @@ -291,6 +298,49 @@ gold_bonus = 4 shield_bonus = -2 happiness_bonus = 2 +#District +name = Water Park +tooltip = Build Water Park +img_paths = WaterPark.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 10 +vary_img_by_era = 0 +vary_img_by_culture = 0 +advance_prereqs = Miniaturization +dependent_improvs = +buildable_on = coast +custom_height = 88 +defense_bonus_percent = 0 +allow_multiple = 0 +culture_bonus = 2 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 4 +shield_bonus = -2 +happiness_bonus = 2 + +#District +name = Wind Farm +tooltip = Build Wind Farm +img_paths = WindFarm.pcx +animation = ini=Districts/WindFarm/WindFarm.INI; hours=7-17 +animation = ini=Districts/WindFarm/WindFarm_night.INI; hours=18-6 +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 10 +vary_img_by_era = 0 +vary_img_by_culture = 0 +advance_prereqs = Ecology +dependent_improvs = +buildable_on = coast,desert,plains,grassland,tundra,hills +defense_bonus_percent = 0 +allow_multiple = 0 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 3,hills:1 +happiness_bonus = 0 + [========================================================================] [=========================SPECIAL DISTRICTS==============================] [========================================================================] diff --git a/default.districts_natural_wonders_config.txt b/default.districts_natural_wonders_config.txt index 8a612072..5c960ede 100644 --- a/default.districts_natural_wonders_config.txt +++ b/default.districts_natural_wonders_config.txt @@ -24,6 +24,12 @@ ; - happiness_bonus : Number. Happiness bonus when worked. ; - impassable : 0 or 1. If 1, completed natural wonder tile is impassable to units. (default: 0) ; - impassable_to_wheeled : 0 or 1. If 1, completed natural wonder tile is impassable to wheeled units unless connected by road. (default: 0) + ; - animation : Text. Optional custom animation entry; may be repeated multiple times per wonder (up to 8). + ; Format: ini=; hours=; seasons=; direction=; frame_time_seconds=; offsets= + ; Subkeys also accept ':' instead of '='. Seasons: summer, fall, winter, spring. + ; Directions: northeast, east, southeast, south, southwest, west, northwest, north. + ; Example: + ; animation = ini=NaturalWonders\AngelFalls.ini; hours=1,2,3 seasons=spring,summer; direction=south; frame_time_seconds=0.12; offsets=0,-10 ] #Wonder @@ -40,6 +46,8 @@ food_bonus = 0 gold_bonus = 0 shield_bonus = 0 happiness_bonus = 1 +animation = ini=NaturalWonders\AngelFalls.INI; hours=7-17; seasons=spring,summer,fall,winter; offsets=0,-10; direction=southwest; frame_time_seconds=0.13 +animation = ini=NaturalWonders\AngelFalls_night.INI; hours=18-6; seasons=spring,summer,fall,winter; offsets=0,-10; direction=southwest; frame_time_seconds=0.13 #Wonder name = Yosemite @@ -276,4 +284,4 @@ science_bonus = 1 food_bonus = 0 gold_bonus = 0 shield_bonus = 0 -happiness_bonus = 1 \ No newline at end of file +happiness_bonus = 1 diff --git a/default.tile_animations.txt b/default.tile_animations.txt new file mode 100644 index 00000000..d1f1eb05 --- /dev/null +++ b/default.tile_animations.txt @@ -0,0 +1,184 @@ +[======================================================================= NOTE =======================================================================] +[Instead of editing this file, changes to the settings should be placed in either the scenario or user config files. The scenario config file must ] +[be named scenario.tile_animations.txt and must be located in your scenario search folder. Of course it only applies if you are using a scenario. ] +[The user config file must be named user.tile_animations.txt and located in the C3X folder, which is the folder where this file is. When creating ] +[scenario or user configs, note that all tile animations defined here will be removed and only your scenario or user-defined animations will be used ] +[====================================================================================================================================================] + +[ + ; Tile Animation config fields (each Animation block begins with "#Animation") + ; - name : Text (required). Internal animation name; must be unique. + ; - ini_path : INI filename under Art/Animations/ (required). + ; - type : Text (required). "terrain", "resource", "pcx", or "coastal-wave". + ; - terrain: animation can appear on matching terrain tiles. + ; - resource: animation replaces the static resource PCX draw on matching resource tiles. + ; - pcx: animation appears on tiles where a specific map sprite sheet/index is actually drawn. + ; - coastal-wave: animation appears on coast tiles and auto-computes its facing from diagonal coastline shape. + ; - resource_type : Text. Resource name. Required if type = resource. + ; - pcx_file : Text. Required if type = pcx. Use the exact Civ3 Art\Terrain filename. Supported values: + ; deltaRivers.pcx + ; floodplains.pcx + ; LMHills.pcx + ; Mountains.pcx + ; Mountains-snow.pcx + ; mtnRivers.pcx + ; Volcanos.pcx + ; Volcanos-snow.pcx + ; waterfalls.pcx + ; xhills.pcx + ; - pcx_index : Integer. Required if type = pcx. + ; These are zero-based, so the first image is index 0, second is 1, etc. + ; How to find index from visual order (top-left=0, then row-major): + ; - default: pcx_index = visual_index + ; - deltaRivers.pcx & mtnRivers.pcx: these are combined within the game and may take trial-and-error to find index. + ; - terrain_types : Comma-delimited terrain types. Required if type = terrain. + ; Valid values: desert, plains, grassland, jungle, tundra, floodplain, swamp, hills, mountains, + ; forest, volcano, snow-forest, snow-mountains, snow-volcano, coast, sea, ocean, land + ; Singular/plural forms are both accepted (for example, volcano/volcanoes). + ; "land" means any non-water tile. + ; - adjacent_to : Text. Optional adjacency requirements. + ; Comma-delimited values using: + ; + ; or + ; : + ; Direction values: northeast, east, southeast, south, southwest, west, northwest, north + ; Example: tundra:northwest, desert:southwest + ; - direction : Text. Optional animation facing override. Valid values: northeast, east, southeast, south, southwest, west, northwest, north + ; - x_offset : Integer. Optional horizontal pixel offset after centering (positive moves right). + ; - y_offset : Integer. Optional vertical pixel offset after centering (positive moves down). + ; - frame_time_seconds : Float. Optional playback frame time. Lower values animate faster. (default: 0.15) + ; - show_in_day_night_hours : Comma-delimited numbers [0..23] and/or inclusive ranges like 6-17. + ; Optional allowed day/night cycle hours. Example: 0-5,19-23 + ; - show_in_seasons : Comma-delimited seasons. Optional allowed seasons: spring, summer, fall, winter. + ; + ; Note: A given tile can have multiple potential animations, depending on the configuration and whether a tile + ; has certain resources, the time of day, season, and so on. Determining which to show is done by scoring + ; system with the following rules: + ; + ; 1. Resource > Natural Wonder > PCX > Terrain > Coastal Wave + ; 2. If two candidate animations are tied, the one with seasons and/or day-night hours specified takes precedence. + ; 3. If still tied, whichever animation appears last takes precedence. +] + +#Animation +name = Wave +ini_path = Terrain\Wave\Wave.INI +type = coastal-wave +show_in_day_night_hours = 7-17 + +#Animation +name = Wave_night +ini_path = Terrain\Wave\Wave_night.INI +type = coastal-wave +show_in_day_night_hours = 18-6 + +#Animation +name = Snow +ini_path = Terrain\Snow\Snow.INI +type = terrain +terrain_types = mountains, snow-mountains, volcanoes, snow-volcanoes +show_in_seasons = winter +show_in_day_night_hours = 7-17 + +#Animation +name = Whales +ini_path = Resources\Whale\Whale.INI +type = resource +resource_type = Whales + +#Animation +name = Deer +ini_path = Resources\Deer\Deer.INI +type = resource +resource_type = Game +direction = southwest +y_offset = -12 +frame_time_seconds = 0.14 +show_in_day_night_hours = 7-17 + +#Animation +name = Deer_night +ini_path = Resources\Deer\Deer_night.INI +type = resource +resource_type = Game +direction = southwest +y_offset = -12 +frame_time_seconds = 0.14 +show_in_day_night_hours = 18-6 + +#Animation +name = Cattle +;ini_path = Resources\MilkCow\MilkCow.INI +ini_path = Resources\Cow\black_and_white_cow.INI +type = resource +resource_type = Cattle +direction = southeast +frame_time_seconds = 0.2 +show_in_day_night_hours = 7-17 + +#Animation +name = Cattle_night +ini_path = Resources\MilkCow\MilkCow_night.INI +type = resource +resource_type = Cattle +direction = southeast +show_in_day_night_hours = 18-6 + +#Animation +name = Horses +ini_path = Resources\HorsePainted\HorsePainted.INI +type = resource +resource_type = Horses +direction = southeast +show_in_day_night_hours = 7-17 + +#Animation +name = Horses_night +ini_path = Resources\HorsePainted\HorsePainted_night.INI +type = resource +resource_type = Horses +direction = southeast +show_in_day_night_hours = 18-6 + +#Animation +name = Ivory +ini_path = Resources\Elephant\Elephant.ini +type = resource +resource_type = Ivory +direction = southwest +frame_time_seconds = 0.17 +show_in_day_night_hours = 7-17 + +#Animation +name = Fish +ini_path = Resources\Fish\Fish.INI +type = resource +resource_type = Fish +direction = southeast +show_in_day_night_hours = 7-17 +frame_time_seconds = 0.13 +y_offset = -20 +x_offset = -15 + +#Animation +name = DestructInitial +ini_path = Destruction\DestructInitial.INI +type = destruct-initial + +#Animation +name = DestructAfter +ini_path = Destruction\DestructAfter.INI +type = destruct-after +y_offset = 45 + +;===Example PCX animation of river segment=== +;#Animation +;name = DeltaRivers_12 +;ini_path = Terrain\DeltaRivers\DeltaRivers_12.INI +;type = pcx +;pcx_file = deltaRivers.pcx +;pcx_index = 12 +;y_offset = 12 +;x_offset = -65 +;show_in_day_night_hours = 7-17 +;frame_time_seconds = 0.15 diff --git a/injected_code.c b/injected_code.c index e8e1a144..a86a3dbf 100644 --- a/injected_code.c +++ b/injected_code.c @@ -229,6 +229,7 @@ bool find_civ_trait_id_by_name (struct string_slice const * name, int * out_id); bool find_civ_culture_id_by_name (struct string_slice const * name, int * out_id); Tile * find_tile_for_district (City * city, int district_id, int * out_x, int * out_y); struct district_instance * get_district_instance (Tile * tile); +bool district_instance_get_coords (struct district_instance * inst, Tile * tile, int * out_x, int * out_y); struct named_tile_entry * get_named_tile_entry (Tile * tile); bool city_has_required_district (City * city, int district_id); bool district_is_complete (Tile * tile, int district_id); @@ -247,6 +248,38 @@ int count_neighborhoods_in_city_radius (City * city); int count_utilized_neighborhoods_in_city_radius (City * city); char * copy_trimmed_string_or_null (struct string_slice const * slice, int remove_quotes); bool city_has_resource_r (City * city, int resource_id, int max_generated_resource_id); +void load_tile_animation_configs (); +bool tile_has_matching_resource_animation_for_draw (Tile * tile, int tile_x, int tile_y); +bool tile_has_matching_resource_animation_for_draw_with_resource (Tile * tile, int tile_x, int tile_y, int resource_id, int * out_effect_id); +void tile_animation_scheduler_tick (); +void rebuild_tile_animation_rule_match_cache (); +void free_tile_animation_selected_matrix (); +void clear_tile_animation_pcx_sprite_lookup (); +void refresh_tile_animation_pcx_rule_mask (); +void __fastcall patch_Tile_spawn_animated_effect (Tile * this, int edx, enum AnimatedEffect effect, int tile_x, int tile_y, bool randomize_start_frame, enum direction dummy_dir); +void reset_tile_animation_runtime_state (); +void trigger_tile_destruct_animation (int tile_x, int tile_y, int trigger); +void spawn_selected_tile_animation_for_tile (int tile_x, int tile_y, bool destruct_only); +void clear_tile_destruct_animation (int tile_x, int tile_y); +void refresh_tile_animation_selection_for_tile (int tile_x, int tile_y); +void age_tile_destruct_animations (); +void ensure_tile_destruct_animation_ages (); +bool tile_has_destruct_animation_age (int tile_index, int age); +bool tile_has_any_destruct_animation_age (int tile_index); +bool tile_animation_matches_time_filters (struct tile_animation_config const * cfg); +bool tile_animation_cache_needs_rebuild (); +void clear_tile_animation_pcx_matches_in_cache (); +void register_tile_animation_pcx_draw_for_current_tile (Sprite * sprite); +void rebuild_tile_animation_pcx_sprite_lookup (); +void refresh_tile_animation_pcx_active_mask (); +int pick_tile_animation_winner_for_tile (unsigned int * tile_mask); +int get_tile_animation_type_priority (enum tile_animation_type type); +bool parse_tile_animation_hour_list (struct string_slice const * value, unsigned int * out_mask); +bool parse_tile_animation_season_list (struct string_slice const * value, unsigned int * out_mask); +bool parse_tile_animation_culture_group_list (struct string_slice const * value, unsigned int * out_mask); +bool parse_tile_animation_era_list (struct string_slice const * value, unsigned int * out_mask); +bool parse_natural_wonder_animation_entry (struct string_slice const * value, struct natural_wonder_animation_config * out_cfg); +struct tile_animation_config * get_tile_animation_for_effect (int effect_id); struct pause_for_popup { bool done; // Set to true to exit for loop @@ -1706,6 +1739,54 @@ read_ai_multi_start_extra_palaces (struct string_slice const * s, return success; } +struct parsable_field_bit { + char * name; + int bit_value; +}; + +bool +read_bit_field (struct string_slice const * s, struct parsable_field_bit const * bits, int count_bits, int * out_field) +{ + struct string_slice trimmed = trim_string_slice (s, 0); + s = &trimmed; + + int tr; + if (s->len <= 0) + tr = 0; + else if (slice_matches_str (s, "all")) + tr = ~0; + else { + tr = 0; + char * cursor = &s->str[0]; + char * s_end = &s->str[s->len]; + while (1) { + struct string_slice name; + + if (cursor >= s_end) + break; + else if (! parse_string (&cursor, &name)) { + skip_white_space (&cursor); + if (cursor >= s_end) + break; + else + return false; // Invalid character in value + } + + bool matched_any = false; + for (int n = 0; n < count_bits; n++) + if (slice_matches_str (&name, bits[n].name)) { + tr |= bits[n].bit_value; + matched_any = true; + break; + } + if (! matched_any) + return false; + } + } + *out_field = tr; + return true; +} + bool read_retreat_rules (struct string_slice const * s, int * out_val) { @@ -1789,6 +1870,49 @@ read_day_night_cycle_mode (struct string_slice const * s, int * out_val) return false; } +bool +read_seasonal_cycle_mode (struct string_slice const * s, int * out_val) +{ + struct string_slice trimmed = trim_string_slice (s, 1); + if (slice_matches_str (&trimmed, "off" )) { *out_val = SCM_OFF; return true; } + else if (slice_matches_str (&trimmed, "timer" )) { *out_val = SCM_TIMER; return true; } + else if (slice_matches_str (&trimmed, "user-season" )) { *out_val = SCM_USER_SEASON; return true; } + else if (slice_matches_str (&trimmed, "every-turn" )) { *out_val = SCM_EVERY_TURN; return true; } + else if (slice_matches_str (&trimmed, "on-day-night-hour")) { *out_val = SCM_ON_DAY_NIGHT_HOUR; return true; } + else if (slice_matches_str (&trimmed, "specified" )) { *out_val = SCM_SPECIFIED; return true; } + else + return false; +} + +bool +read_enabled_seasons_mask (struct string_slice const * s, int * out_val) +{ + struct parsable_field_bit bits[] = { + {"summer", 1 << CS_SUMMER}, + {"fall" , 1 << CS_FALL}, + {"winter", 1 << CS_WINTER}, + {"spring", 1 << CS_SPRING}, + }; + return read_bit_field (s, bits, ARRAY_LEN (bits), out_val); +} + +bool +read_pinned_season_for_seasonal_cycle (struct string_slice const * s, int * out_val) +{ + struct string_slice trimmed = trim_string_slice (s, 1); + if (slice_matches_str (&trimmed, "summer")) { *out_val = CS_SUMMER; return true; } + else if (slice_matches_str (&trimmed, "Summer")) { *out_val = CS_SUMMER; return true; } + else if (slice_matches_str (&trimmed, "fall" )) { *out_val = CS_FALL; return true; } + else if (slice_matches_str (&trimmed, "Fall" )) { *out_val = CS_FALL; return true; } + else if (slice_matches_str (&trimmed, "autumn")) { *out_val = CS_FALL; return true; } + else if (slice_matches_str (&trimmed, "autumn")) { *out_val = CS_FALL; return true; } + else if (slice_matches_str (&trimmed, "winter")) { *out_val = CS_WINTER; return true; } + else if (slice_matches_str (&trimmed, "Winter")) { *out_val = CS_WINTER; return true; } + else if (slice_matches_str (&trimmed, "spring")) { *out_val = CS_SPRING; return true; } + else if (slice_matches_str (&trimmed, "Spring")) { *out_val = CS_SPRING; return true; } + return false; +} + bool read_distribution_hub_yield_division_mode (struct string_slice const * s, int * out_val) { @@ -1856,7 +1980,7 @@ read_tile_terrain_type_value (struct string_slice const * s, enum SquareTypes * {"swamp", SQ_Swamp}, {"swamps", SQ_Swamp}, {"volcano", SQ_Volcano}, - {"volcanos", SQ_Volcano}, + {"volcanoes", SQ_Volcano}, {"coast", SQ_Coast}, {"coasts", SQ_Coast}, {"sea", SQ_Sea}, @@ -1866,7 +1990,7 @@ read_tile_terrain_type_value (struct string_slice const * s, enum SquareTypes * {"river", SQ_RIVER}, {"rivers", SQ_RIVER}, {"snow-volcano", SQ_SNOW_VOLCANO}, - {"snow-volcanos", SQ_SNOW_VOLCANO}, + {"snow-volcanoes", SQ_SNOW_VOLCANO}, {"snow-forest", SQ_SNOW_FOREST}, {"snow-forests", SQ_SNOW_FOREST}, {"snow-mountain", SQ_SNOW_MOUNTAIN}, @@ -2162,52 +2286,10 @@ read_barbarian_activity_override (struct string_slice const * s, enum barbarian_ return found; } -struct parsable_field_bit { - char * name; - int bit_value; -}; - bool -read_bit_field (struct string_slice const * s, struct parsable_field_bit const * bits, int count_bits, int * out_field) +read_tile_animation_direction_value (struct string_slice const * s, enum direction * out_dir) { - struct string_slice trimmed = trim_string_slice (s, 0); - s = &trimmed; - - int tr; - if (s->len <= 0) - tr = 0; - else if (slice_matches_str (s, "all")) - tr = ~0; - else { - tr = 0; - char * cursor = &s->str[0]; - char * s_end = &s->str[s->len]; - while (1) { - struct string_slice name; - - if (cursor >= s_end) - break; - else if (! parse_string (&cursor, &name)) { - skip_white_space (&cursor); - if (cursor >= s_end) - break; - else - return false; // Invalid character in value - } - - bool matched_any = false; - for (int n = 0; n < count_bits; n++) - if (slice_matches_str (&name, bits[n].name)) { - tr |= bits[n].bit_value; - matched_any = true; - break; - } - if (! matched_any) - return false; - } - } - *out_field = tr; - return true; + return read_direction_value (s, out_dir); } int @@ -2464,6 +2546,16 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) }; if (! read_bit_field (&value, bits, ARRAY_LEN (bits), (int *)&cfg->special_defensive_bombard_rules)) handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "show_tile_destruct_animation_after")) { + struct parsable_field_bit bits[] = { + {"bombard", TDAT_BOMBARD}, + {"pillage", TDAT_PILLAGE}, + {"bomb" , TDAT_BOMB}, + }; + struct string_slice trimmed = trim_string_slice (&value, 1); + if (slice_matches_str (&trimmed, "all") || + (! read_bit_field (&value, bits, ARRAY_LEN (bits), &cfg->show_tile_destruct_animation_after))) + handle_config_error (&p, CPE_BAD_VALUE); } else if (slice_matches_str (&p.key, "special_zone_of_control_rules")) { struct parsable_field_bit bits[] = { {"lethal" , SZOCR_LETHAL}, @@ -2505,6 +2597,15 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) } else if (slice_matches_str (&p.key, "day_night_cycle_mode")) { if (! read_day_night_cycle_mode (&value, (int *)&cfg->day_night_cycle_mode)) handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "seasonal_cycle_mode")) { + if (! read_seasonal_cycle_mode (&value, (int *)&cfg->seasonal_cycle_mode)) + handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "enabled_seasons")) { + if (! read_enabled_seasons_mask (&value, (int *)&cfg->enabled_seasons_mask)) + handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "pinned_season_for_seasonal_cycle")) { + if (! read_pinned_season_for_seasonal_cycle (&value, (int *)&cfg->pinned_season_for_seasonal_cycle)) + handle_config_error (&p, CPE_BAD_VALUE); } else if (slice_matches_str (&p.key, "distribution_hub_yield_division_mode")) { if (! read_distribution_hub_yield_division_mode (&value, (int *)&cfg->distribution_hub_yield_division_mode)) handle_config_error (&p, CPE_BAD_VALUE); @@ -2886,8 +2987,12 @@ remove_district_instance (Tile * tile) struct district_instance * inst = get_district_instance (tile); if (inst != NULL) { + int tile_x = -1, tile_y = -1; + bool has_coords = district_instance_get_coords (inst, tile, &tile_x, &tile_y); free (inst); itable_remove (&is->district_tile_map, (int)tile); + if (has_coords) + refresh_tile_animation_selection_for_tile (tile_x, tile_y); } } @@ -6434,6 +6539,7 @@ district_is_complete(Tile * tile, int district_id) char ss[200]; snprintf (ss, sizeof ss, "District %d completed at tile (%d,%d)\n", district_id, tile_x, tile_y); (*p_OutputDebugStringA) (ss); + refresh_tile_animation_selection_for_tile (tile_x, tile_y); // Check if this was an AI-requested district struct pending_district_request * req = find_pending_district_request_by_coords (NULL, tile_x, tile_y, district_id); @@ -7241,6 +7347,41 @@ move_bonus_entry_list (struct district_bonus_list * dest, src->count = 0; } +void +free_animation_config_entries (struct natural_wonder_animation_config * animations, int * animation_count) +{ + if ((animations == NULL) || (animation_count == NULL)) + return; + + for (int i = 0; i < *animation_count; i++) { + if (animations[i].ini_path != NULL) { + free ((void *)animations[i].ini_path); + animations[i].ini_path = NULL; + } + } + *animation_count = 0; +} + +void +move_animation_config_entries (struct natural_wonder_animation_config * dest, + int * dest_count, + struct natural_wonder_animation_config * src, + int * src_count, + int dest_capacity) +{ + if ((dest == NULL) || (dest_count == NULL) || (src == NULL) || (src_count == NULL)) + return; + + *dest_count = *src_count; + if (*dest_count > dest_capacity) + *dest_count = dest_capacity; + for (int i = 0; i < *dest_count; i++) { + dest[i] = src[i]; + src[i].ini_path = NULL; + } + *src_count = 0; +} + void free_dynamic_district_config (struct district_config * cfg) { @@ -7366,6 +7507,7 @@ free_dynamic_district_config (struct district_config * cfg) free_bonus_entry_list (&cfg->shield_bonus_extras); free_bonus_entry_list (&cfg->happiness_bonus_extras); free_bonus_entry_list (&cfg->defense_bonus_extras); + free_animation_config_entries (cfg->animations, &cfg->animation_count); memset (cfg, 0, sizeof *cfg); } @@ -7431,6 +7573,7 @@ free_dynamic_natural_wonder_config (struct natural_wonder_district_config * cfg) free ((void *)cfg->img_path); cfg->img_path = NULL; } + free_animation_config_entries (cfg->animations, &cfg->animation_count); memset (cfg, 0, sizeof *cfg); cfg->adjacent_to = (enum SquareTypes)SQ_INVALID; @@ -7588,6 +7731,7 @@ free_special_district_override_strings (struct district_config * cfg, struct dis free_bonus_entry_list_override (&cfg->shield_bonus_extras, &defaults->shield_bonus_extras); free_bonus_entry_list_override (&cfg->happiness_bonus_extras, &defaults->happiness_bonus_extras); free_bonus_entry_list_override (&cfg->defense_bonus_extras, &defaults->defense_bonus_extras); + free_animation_config_entries (cfg->animations, &cfg->animation_count); } void @@ -7800,6 +7944,7 @@ free_parsed_district_definition (struct parsed_district_definition * def) free_bonus_entry_list (&def->shield_bonus_extras); free_bonus_entry_list (&def->happiness_bonus_extras); free_bonus_entry_list (&def->defense_bonus_extras); + free_animation_config_entries (def->animations, &def->animation_count); init_parsed_district_definition (def); } @@ -8619,6 +8764,12 @@ override_special_district_from_definition (struct parsed_district_definition * d free_bonus_entry_list_override (&cfg->happiness_bonus_extras, &defaults->happiness_bonus_extras); move_bonus_entry_list (&cfg->happiness_bonus_extras, &def->happiness_bonus_extras); } + if (def->animation_count > 0) { + free_animation_config_entries (cfg->animations, &cfg->animation_count); + move_animation_config_entries (cfg->animations, &cfg->animation_count, + def->animations, &def->animation_count, + ARRAY_LEN (cfg->animations)); + } if (def->has_buildable_on) cfg->buildable_square_types_mask = def->buildable_square_types_mask; if (def->has_buildable_adjacent_to) { @@ -8920,6 +9071,10 @@ add_dynamic_district_from_definition (struct parsed_district_definition * def, i if (def->has_defense_bonus_percent) move_bonus_entry_list (&new_cfg.defense_bonus_extras, &def->defense_bonus_extras); + move_animation_config_entries (new_cfg.animations, &new_cfg.animation_count, + def->animations, &def->animation_count, + ARRAY_LEN (new_cfg.animations)); + if (def->has_generated_resource) { new_cfg.generated_resource = def->generated_resource; def->generated_resource = NULL; @@ -9570,6 +9725,17 @@ handle_district_definition_key (struct parsed_district_definition * def, } else add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } else if (slice_matches_str (key, "animation")) { + if (def->animation_count >= ARRAY_LEN (def->animations)) + add_key_parse_error (parse_errors, line_number, key, value, "(too many animations for one district)"); + else { + struct natural_wonder_animation_config anim = {0}; + if (parse_natural_wonder_animation_entry (value, &anim)) + def->animations[def->animation_count++] = anim; + else + add_key_parse_error (parse_errors, line_number, key, value, "(expected \"ini:; hours:<0..23 list>; seasons:; cultures:; eras:\")"); + } + } else if (slice_matches_str (key, "custom_width")) { struct string_slice val_slice = *value; int ival; @@ -10714,6 +10880,14 @@ init_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * void free_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * def) { + for (int i = 0; i < def->animation_count; i++) { + if (def->animations[i].ini_path != NULL) { + free ((void *)def->animations[i].ini_path); + def->animations[i].ini_path = NULL; + } + } + def->animation_count = 0; + if (def->name != NULL) { free (def->name); def->name = NULL; @@ -10762,6 +10936,34 @@ add_natural_wonder_from_definition (struct parsed_natural_wonder_definition * de new_cfg.terrain_type = def->terrain_type; new_cfg.adjacent_to = def->adjacent_to; new_cfg.adjacency_dir = def->adjacency_dir; + new_cfg.animation_count = def->animation_count; + if (new_cfg.animation_count > ARRAY_LEN (new_cfg.animations)) + new_cfg.animation_count = ARRAY_LEN (new_cfg.animations); + for (int i = 0; i < new_cfg.animation_count; i++) { + struct natural_wonder_animation_config const * src_anim = &def->animations[i]; + char * ini_copy = NULL; + if (src_anim->ini_path != NULL) + ini_copy = strdup (src_anim->ini_path); + if ((src_anim->ini_path != NULL) && (ini_copy == NULL)) { + for (int j = 0; j < i; j++) { + if (new_cfg.animations[j].ini_path != NULL) + free ((void *)new_cfg.animations[j].ini_path); + } + free (img_copy); + free (name_copy); + return false; + } + new_cfg.animations[i].ini_path = ini_copy; + new_cfg.animations[i].day_night_hour_mask = src_anim->day_night_hour_mask; + new_cfg.animations[i].season_mask = src_anim->season_mask; + new_cfg.animations[i].direction = src_anim->direction; + new_cfg.animations[i].has_direction = src_anim->has_direction; + new_cfg.animations[i].frame_time_seconds = src_anim->frame_time_seconds; + new_cfg.animations[i].has_frame_time_seconds = src_anim->has_frame_time_seconds; + new_cfg.animations[i].x_offset = src_anim->x_offset; + new_cfg.animations[i].y_offset = src_anim->y_offset; + new_cfg.animations[i].has_offsets = src_anim->has_offsets; + } new_cfg.culture_bonus = def->has_culture_bonus ? def->culture_bonus : 0; new_cfg.science_bonus = def->has_science_bonus ? def->science_bonus : 0; new_cfg.food_bonus = def->has_food_bonus ? def->food_bonus : 0; @@ -10832,6 +11034,134 @@ finalize_parsed_natural_wonder_definition (struct parsed_natural_wonder_definiti free_parsed_natural_wonder_definition (def); } +bool +parse_natural_wonder_animation_entry (struct string_slice const * value, + struct natural_wonder_animation_config * out_cfg) +{ + if ((value == NULL) || (out_cfg == NULL)) + return false; + + memset (out_cfg, 0, sizeof *out_cfg); + struct string_slice trimmed_value = trim_string_slice (value, 1); + char * text = extract_slice (&trimmed_value); + if (text == NULL) + return false; + + bool ok = true; + bool has_ini = false; + char * cursor = text; + while (ok && (*cursor != '\0')) { + char * token_start = cursor; + while ((*cursor != '\0') && (*cursor != ';')) + cursor++; + char saved = *cursor; + *cursor = '\0'; + + struct string_slice token = {.str = token_start, .len = strlen (token_start)}; + token = trim_string_slice (&token, 0); + if (token.len > 0) { + char * sep = NULL; + for (int i = 0; i < token.len; i++) { + char ch = token.str[i]; + if ((ch == ':') || (ch == '=')) { + sep = token.str + i; + break; + } + } + + if (sep == NULL) { + ok = false; + } else { + struct string_slice k = {.str = token.str, .len = sep - token.str}; + struct string_slice v = {.str = sep + 1, .len = strlen (sep + 1)}; + k = trim_string_slice (&k, 0); + v = trim_string_slice (&v, 0); + if (slice_matches_str (&k, "ini")) { + if (out_cfg->ini_path != NULL) { + free ((void *)out_cfg->ini_path); + out_cfg->ini_path = NULL; + } + out_cfg->ini_path = extract_slice (&v); + has_ini = (out_cfg->ini_path != NULL) && (out_cfg->ini_path[0] != '\0'); + if (! has_ini) + ok = false; + } else if (slice_matches_str (&k, "hours")) { + unsigned int mask = 0; + ok = parse_tile_animation_hour_list (&v, &mask); + if (ok) + out_cfg->day_night_hour_mask = mask; + } else if (slice_matches_str (&k, "seasons")) { + unsigned int mask = 0; + ok = parse_tile_animation_season_list (&v, &mask); + if (ok) + out_cfg->season_mask = mask; + } else if (slice_matches_str (&k, "cultures") || slice_matches_str (&k, "culture")) { + unsigned int mask = 0; + ok = parse_tile_animation_culture_group_list (&v, &mask); + if (ok) + out_cfg->culture_group_mask = mask; + } else if (slice_matches_str (&k, "eras") || slice_matches_str (&k, "era")) { + unsigned int mask = 0; + ok = parse_tile_animation_era_list (&v, &mask); + if (ok) + out_cfg->era_mask = mask; + } else if (slice_matches_str (&k, "direction")) { + enum direction dir = DIR_ZERO; + ok = read_direction_value (&v, &dir); + if (ok) { + out_cfg->direction = dir; + out_cfg->has_direction = true; + } + } else if (slice_matches_str (&k, "frame_time_seconds")) { + float frame_time_seconds = 0.0f; + ok = read_float (&v, &frame_time_seconds); + if (ok) { + out_cfg->frame_time_seconds = frame_time_seconds; + out_cfg->has_frame_time_seconds = true; + } + } else if (slice_matches_str (&k, "offsets")) { + char * offsets_text = extract_slice (&v); + if (offsets_text == NULL) + ok = false; + else { + char * off_cursor = offsets_text; + int x = 0, y = 0; + ok = parse_int (&off_cursor, &x) && + skip_horiz_space (&off_cursor) && + (*off_cursor == ','); + if (ok) { + off_cursor++; + ok = parse_int (&off_cursor, &y) && + skip_horiz_space (&off_cursor) && + (*off_cursor == '\0'); + } + if (ok) { + out_cfg->x_offset = x; + out_cfg->y_offset = y; + out_cfg->has_offsets = true; + } + free (offsets_text); + } + } else + ok = false; + } + } + + if (saved == ';') + cursor++; + } + + free (text); + if ((! ok) || (! has_ini)) { + if (out_cfg->ini_path != NULL) { + free ((void *)out_cfg->ini_path); + out_cfg->ini_path = NULL; + } + return false; + } + return true; +} + void handle_natural_wonder_definition_key (struct parsed_natural_wonder_definition * def, struct string_slice const * key, @@ -11024,6 +11354,17 @@ handle_natural_wonder_definition_key (struct parsed_natural_wonder_definition * add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); } + } else if (slice_matches_str (key, "animation")) { + if (def->animation_count >= ARRAY_LEN (def->animations)) + add_key_parse_error (parse_errors, line_number, key, value, "(too many animations for one wonder)"); + else { + struct natural_wonder_animation_config anim = {0}; + if (parse_natural_wonder_animation_entry (value, &anim)) + def->animations[def->animation_count++] = anim; + else + add_key_parse_error (parse_errors, line_number, key, value, "(expected \"ini:; hours:<0..23 list>; seasons:\")"); + } + } else add_unrecognized_key_error (unrecognized_keys, line_number, key); } @@ -14987,6 +15328,7 @@ handle_district_removed (Tile * tile, int district_id, int center_x, int center_ tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, center_x, center_y); tile->vtable->m60_Set_Ruins (tile, __, 1); } + refresh_tile_animation_selection_for_tile (center_x, center_y); p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); } @@ -16858,7 +17200,7 @@ read_in_dir(PCX_Image *img, PCX_Image_read_file(img, __, temp_path, NULL, 0, 0x100, 2); } -bool load_day_night_hour_images(struct day_night_cycle_img_set *this, const char *art_dir, const char *hour) +bool load_day_night_hour_and_season_images(struct day_night_cycle_img_set *this, const char *art_dir, const char *season, const char *hour) { char ss[200]; PCX_Image img; @@ -17153,7 +17495,7 @@ bool load_day_night_hour_images(struct day_night_cycle_img_set *this, const char if (is->current_config.enable_districts) { char art_dir[200]; char temp_path[2*MAX_PATH]; - snprintf (art_dir, sizeof art_dir, "Districts/%s", hour); + snprintf (art_dir, sizeof art_dir, "Districts/%s/%s", season, hour); get_mod_art_path (art_dir, temp_path, sizeof temp_path); for (int dc = 0; dc < is->district_count; dc++) { struct district_config const * cfg = &is->district_configs[dc]; @@ -17262,7 +17604,7 @@ bool load_day_night_hour_images(struct day_night_cycle_img_set *this, const char if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { char art_dir[200]; char temp_path[2*MAX_PATH]; - snprintf (art_dir, sizeof art_dir, "Districts/%s", hour); + snprintf (art_dir, sizeof art_dir, "Districts/%s/%s", season, hour); get_mod_art_path (art_dir, temp_path, sizeof temp_path); char const * last_img_path = NULL; @@ -17311,169 +17653,279 @@ bool load_day_night_hour_images(struct day_night_cycle_img_set *this, const char } Sprite * -get_sprite_proxy_for_current_hour(Sprite *s) { - int v; - int hour = is->current_day_night_cycle; // 0..23 - if (itable_look_up(&is->day_night_sprite_proxy_by_hour[hour], (int)s, &v)) - return (Sprite *)v; - return NULL; // not proxied, fall back to s +get_cycle_sprite_proxy(Sprite *s) { + if (is->current_config.day_night_cycle_mode == DNCM_OFF && is->current_config.seasonal_cycle_mode == SCM_OFF) + return NULL; + if (is->day_night_sprite_proxy_by_season_and_hour == NULL) + return NULL; + + int season = (is->current_config.seasonal_cycle_mode != SCM_OFF) ? clamp (0, 3, is->current_seasonal_cycle) : CS_SUMMER; + int hour = (is->current_config.day_night_cycle_mode != DNCM_OFF) ? clamp (0, 23, is->current_day_night_cycle) : 12; + int cycle_idx = 24 * season + hour; + int v; + if (itable_look_up (&is->day_night_sprite_proxy_by_season_and_hour[cycle_idx], (int)s, &v)) + return (Sprite *)v; + return NULL; } void -insert_spritelist_proxies(SpriteList *ss, SpriteList *ps, int hour, int len1, int len2) { +insert_spritelist_proxies(SpriteList *ss, SpriteList *ps, int season, int hour, int len1, int len2) { + if (is->day_night_sprite_proxy_by_season_and_hour == NULL) + return; + int cycle_idx = 24 * season + hour; for (int i = 0; i < len1; i++) { for (int j = 0; j < len2; j++) { Sprite *s = &ss[i].field_0[j]; Sprite *p = &ps[i].field_0[j]; if (s && p) { - itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); + itable_insert(&is->day_night_sprite_proxy_by_season_and_hour[cycle_idx], (int)s, (int)p); } } } } void -insert_sprite_proxies(Sprite *ss, Sprite *ps, int hour, int len) { +insert_sprite_proxies(Sprite *ss, Sprite *ps, int season, int hour, int len) { + if (is->day_night_sprite_proxy_by_season_and_hour == NULL) + return; + int cycle_idx = 24 * season + hour; for (int i = 0; i < len; i++) { Sprite *s = &ss[i]; Sprite *p = &ps[i]; if (s && p) { - itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); + itable_insert(&is->day_night_sprite_proxy_by_season_and_hour[cycle_idx], (int)s, (int)p); } } } void -insert_sprite_proxy(Sprite *s, Sprite *p, int hour) { +insert_sprite_proxy(Sprite *s, Sprite *p, int season, int hour) { + if (is->day_night_sprite_proxy_by_season_and_hour == NULL) + return; + int cycle_idx = 24 * season + hour; if (s && p) { - itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); + itable_insert(&is->day_night_sprite_proxy_by_season_and_hour[cycle_idx], (int)s, (int)p); } } -void -build_sprite_proxies_24(Map_Renderer *mr) { - for (int h = 0; h < 24; ++h) { - insert_sprite_proxies(city_sprites, is->day_night_cycle_imgs[h].City_Images, h, 80); - insert_sprite_proxies(destroyed_city_sprites, is->day_night_cycle_imgs[h].Destroyed_City_Images, h, 3); - insert_sprite_proxies(mr->Resources, is->day_night_cycle_imgs[h].Resources, h, 36); - insert_spritelist_proxies(mr->Std_Terrain_Images, is->day_night_cycle_imgs[h].Std_Terrain_Images, h, 9, 81); - insert_spritelist_proxies(mr->LM_Terrain_Images, is->day_night_cycle_imgs[h].LM_Terrain_Images, h, 9, 81); - insert_sprite_proxy(&mr->Terrain_Buldings_Barbarian_Camp, &is->day_night_cycle_imgs[h].Terrain_Buldings_Barbarian_Camp, h); - insert_sprite_proxy(&mr->Terrain_Buldings_Mines, &is->day_night_cycle_imgs[h].Terrain_Buldings_Mines, h); - insert_sprite_proxy(&mr->Victory_Image, &is->day_night_cycle_imgs[h].Victory_Image, h); - insert_sprite_proxy(&mr->Terrain_Buldings_Radar, &is->day_night_cycle_imgs[h].Terrain_Buldings_Radar, h); - insert_sprite_proxies(mr->Flood_Plains_Images, is->day_night_cycle_imgs[h].Flood_Plains_Images, h, 16); - insert_sprite_proxies(mr->Polar_Icecaps_Images, is->day_night_cycle_imgs[h].Polar_Icecaps_Images, h, 32); - insert_sprite_proxies(mr->Roads_Images, is->day_night_cycle_imgs[h].Roads_Images, h, 256); - insert_sprite_proxies(mr->Railroads_Images, is->day_night_cycle_imgs[h].Railroads_Images, h, 272); - insert_sprite_proxies(mr->Terrain_Buldings_Airfields, is->day_night_cycle_imgs[h].Terrain_Buldings_Airfields, h, 2); - insert_sprite_proxies(mr->Terrain_Buldings_Camp, is->day_night_cycle_imgs[h].Terrain_Buldings_Camp, h, 4); - insert_sprite_proxies(mr->Terrain_Buldings_Fortress, is->day_night_cycle_imgs[h].Terrain_Buldings_Fortress, h, 4); - insert_sprite_proxies(mr->Terrain_Buldings_Barricade, is->day_night_cycle_imgs[h].Terrain_Buldings_Barricade, h, 4); - insert_sprite_proxies(mr->Goody_Huts_Images, is->day_night_cycle_imgs[h].Goody_Huts_Images, h, 8); - insert_sprite_proxies(mr->Terrain_Buldings_Outposts, is->day_night_cycle_imgs[h].Terrain_Buldings_Outposts, h, 3); - insert_sprite_proxies(mr->Pollution, is->day_night_cycle_imgs[h].Pollution, h, 25); - insert_sprite_proxies(mr->Craters, is->day_night_cycle_imgs[h].Craters, h, 25); - insert_sprite_proxies(mr->Tnt_Images, is->day_night_cycle_imgs[h].Tnt_Images, h, 18); - insert_sprite_proxies(mr->Waterfalls_Images, is->day_night_cycle_imgs[h].Waterfalls_Images, h, 4); - insert_sprite_proxies(mr->LM_Terrain, is->day_night_cycle_imgs[h].LM_Terrain, h, 7); - insert_sprite_proxies(mr->Marsh_Large, is->day_night_cycle_imgs[h].Marsh_Large, h, 8); - insert_sprite_proxies(mr->Marsh_Small, is->day_night_cycle_imgs[h].Marsh_Small, h, 10); - insert_sprite_proxies(mr->Volcanos_Images, is->day_night_cycle_imgs[h].Volcanos_Images, h, 16); - insert_sprite_proxies(mr->Volcanos_Forests_Images, is->day_night_cycle_imgs[h].Volcanos_Forests_Images, h, 16); - insert_sprite_proxies(mr->Volcanos_Jungles_Images, is->day_night_cycle_imgs[h].Volcanos_Jungles_Images, h, 16); - insert_sprite_proxies(mr->Volcanos_Snow_Images, is->day_night_cycle_imgs[h].Volcanos_Snow_Images, h, 16); - insert_sprite_proxies(mr->Grassland_Forests_Large, is->day_night_cycle_imgs[h].Grassland_Forests_Large, h, 8); - insert_sprite_proxies(mr->Plains_Forests_Large, is->day_night_cycle_imgs[h].Plains_Forests_Large, h, 8); - insert_sprite_proxies(mr->Tundra_Forests_Large, is->day_night_cycle_imgs[h].Tundra_Forests_Large, h, 8); - insert_sprite_proxies(mr->Grassland_Forests_Small, is->day_night_cycle_imgs[h].Grassland_Forests_Small, h, 10); - insert_sprite_proxies(mr->Plains_Forests_Small, is->day_night_cycle_imgs[h].Plains_Forests_Small, h, 10); - insert_sprite_proxies(mr->Tundra_Forests_Small, is->day_night_cycle_imgs[h].Tundra_Forests_Small, h, 10); - insert_sprite_proxies(mr->Grassland_Forests_Pines, is->day_night_cycle_imgs[h].Grassland_Forests_Pines, h, 12); - insert_sprite_proxies(mr->Plains_Forests_Pines, is->day_night_cycle_imgs[h].Plains_Forests_Pines, h, 12); - insert_sprite_proxies(mr->Tundra_Forests_Pines, is->day_night_cycle_imgs[h].Tundra_Forests_Pines, h, 12); - insert_sprite_proxies(mr->Irrigation_Desert_Images, is->day_night_cycle_imgs[h].Irrigation_Desert_Images, h, 16); - insert_sprite_proxies(mr->Irrigation_Plains_Images, is->day_night_cycle_imgs[h].Irrigation_Plains_Images, h, 16); - insert_sprite_proxies(mr->Irrigation_Images, is->day_night_cycle_imgs[h].Irrigation_Images, h, 16); - insert_sprite_proxies(mr->Irrigation_Tundra_Images, is->day_night_cycle_imgs[h].Irrigation_Tundra_Images, h, 16); - insert_sprite_proxies(mr->Grassland_Jungles_Large, is->day_night_cycle_imgs[h].Grassland_Jungles_Large, h, 8); - insert_sprite_proxies(mr->Grassland_Jungles_Small, is->day_night_cycle_imgs[h].Grassland_Jungles_Small, h, 12); - insert_sprite_proxies(mr->Mountains_Images, is->day_night_cycle_imgs[h].Mountains_Images, h, 16); - insert_sprite_proxies(mr->Mountains_Forests_Images, is->day_night_cycle_imgs[h].Mountains_Forests_Images, h, 16); - insert_sprite_proxies(mr->Mountains_Jungles_Images, is->day_night_cycle_imgs[h].Mountains_Jungles_Images, h, 16); - insert_sprite_proxies(mr->Mountains_Snow_Images, is->day_night_cycle_imgs[h].Mountains_Snow_Images, h, 16); - insert_sprite_proxies(mr->Hills_Images, is->day_night_cycle_imgs[h].Hills_Images, h, 16); - insert_sprite_proxies(mr->Hills_Forests_Images, is->day_night_cycle_imgs[h].Hills_Forests_Images, h, 16); - insert_sprite_proxies(mr->Hills_Jungle_Images, is->day_night_cycle_imgs[h].Hills_Jungle_Images, h, 16); - insert_sprite_proxies(mr->Delta_Rivers_Images, is->day_night_cycle_imgs[h].Delta_Rivers_Images, h, 16); - insert_sprite_proxies(mr->Mountain_Rivers_Images, is->day_night_cycle_imgs[h].Mountain_Rivers_Images, h, 16); - insert_sprite_proxies(mr->LM_Mountains_Images, is->day_night_cycle_imgs[h].LM_Mountains_Images, h, 16); - insert_sprite_proxies(mr->LM_Forests_Large_Images, is->day_night_cycle_imgs[h].LM_Forests_Large_Images, h, 8); - insert_sprite_proxies(mr->LM_Forests_Small_Images, is->day_night_cycle_imgs[h].LM_Forests_Small_Images, h, 10); - insert_sprite_proxies(mr->LM_Forests_Pines_Images, is->day_night_cycle_imgs[h].LM_Forests_Pines_Images, h, 12); - insert_sprite_proxies(mr->LM_Hills_Images, is->day_night_cycle_imgs[h].LM_Hills_Images, h, 16); - - if (is->current_config.enable_districts) { - for (int dc = 0; dc < is->district_count; dc++) { - struct district_config const * cfg = &is->district_configs[dc]; - int variant_capacity = ARRAY_LEN (is->district_img_sets[dc].imgs); - int variant_count = cfg->img_path_count; - if (variant_count <= 0) - continue; - if (variant_count > variant_capacity) - variant_count = variant_capacity; +bool +allocate_day_night_cycle_runtime_storage () +{ + int count = COUNT_CYCLE_SEASONS * 24; + + if (is->cycle_imgs == NULL) { + is->cycle_imgs = malloc (count * sizeof is->cycle_imgs[0]); + if (is->cycle_imgs == NULL) + return false; + memset (is->cycle_imgs, 0, count * sizeof is->cycle_imgs[0]); + } + + if (is->day_night_sprite_proxy_by_season_and_hour == NULL) { + is->day_night_sprite_proxy_by_season_and_hour = malloc (count * sizeof is->day_night_sprite_proxy_by_season_and_hour[0]); + if (is->day_night_sprite_proxy_by_season_and_hour == NULL) + return false; + memset (is->day_night_sprite_proxy_by_season_and_hour, 0, count * sizeof is->day_night_sprite_proxy_by_season_and_hour[0]); + } + + return true; +} + +int +normalize_enabled_season_mask (int mask) +{ + mask &= 0xF; + if (mask == 0) + mask = 1 << CS_SUMMER; + return mask; +} - int era_count = cfg->vary_img_by_era ? 4 : 1; - int column_count = cfg->img_column_count; +int +get_first_enabled_season (int mask) +{ + mask = normalize_enabled_season_mask (mask); + for (int season = 0; season < COUNT_CYCLE_SEASONS; season++) + if (mask & (1 << season)) + return season; + return CS_SUMMER; +} + +int +get_next_enabled_season (int current_season, int mask) +{ + mask = normalize_enabled_season_mask (mask); + for (int offset = 1; offset <= COUNT_CYCLE_SEASONS; offset++) { + int season = (current_season + offset) % COUNT_CYCLE_SEASONS; + if (mask & (1 << season)) + return season; + } + return CS_SUMMER; +} + +int +get_required_hour_mask_for_cycle_loading () +{ + if (is->current_config.day_night_cycle_mode == DNCM_OFF) + return 1 << 12; + + switch (is->current_config.day_night_cycle_mode) { + case DNCM_SPECIFIED: + return 1 << clamp (0, 23, is->current_config.pinned_hour_for_day_night_cycle); + default: + return (1 << 24) - 1; + } +} + +int +get_required_season_mask_for_cycle_loading () +{ + if (is->current_config.seasonal_cycle_mode == SCM_OFF) + return 1 << CS_SUMMER; - for (int variant_i = 0; variant_i < variant_count; variant_i++) { - if ((cfg->img_paths[variant_i] == NULL) || (cfg->img_paths[variant_i][0] == '\0')) + int enabled_mask = normalize_enabled_season_mask (is->current_config.enabled_seasons_mask); + if (is->current_config.seasonal_cycle_mode == SCM_SPECIFIED) { + int pinned = clamp (CS_SUMMER, CS_SPRING, is->current_config.pinned_season_for_seasonal_cycle); + if ((enabled_mask & (1 << pinned)) != 0) + return 1 << pinned; + return 1 << get_first_enabled_season (enabled_mask); + } + return enabled_mask; +} + +int +get_current_local_season () +{ + SYSTEMTIME st; + GetLocalTime (&st); + int month = st.wMonth; + if ((month == 12) || (month == 1) || (month == 2)) + return CS_WINTER; + else if ((month >= 3) && (month <= 5)) + return CS_SPRING; + else if ((month >= 6) && (month <= 8)) + return CS_SUMMER; + else + return CS_FALL; +} + +void +build_sprite_proxies(Map_Renderer *mr) { + if (is->cycle_imgs == NULL || is->day_night_sprite_proxy_by_season_and_hour == NULL) + return; + + int required_season_mask = get_required_season_mask_for_cycle_loading (); + int required_hour_mask = get_required_hour_mask_for_cycle_loading (); + for (int season = 0; season < COUNT_CYCLE_SEASONS; season++) { + if ((required_season_mask & (1 << season)) == 0) + continue; + for (int h = 0; h < 24; ++h) { + if ((required_hour_mask & (1 << h)) == 0) + continue; + struct day_night_cycle_img_set * set = &is->cycle_imgs[24 * season + h]; + insert_sprite_proxies(city_sprites, set->City_Images, season, h, 80); + insert_sprite_proxies(destroyed_city_sprites, set->Destroyed_City_Images, season, h, 3); + insert_sprite_proxies(mr->Resources, set->Resources, season, h, 36); + insert_spritelist_proxies(mr->Std_Terrain_Images, set->Std_Terrain_Images, season, h, 9, 81); + insert_spritelist_proxies(mr->LM_Terrain_Images, set->LM_Terrain_Images, season, h, 9, 81); + insert_sprite_proxy(&mr->Terrain_Buldings_Barbarian_Camp, &set->Terrain_Buldings_Barbarian_Camp, season, h); + insert_sprite_proxy(&mr->Terrain_Buldings_Mines, &set->Terrain_Buldings_Mines, season, h); + insert_sprite_proxy(&mr->Victory_Image, &set->Victory_Image, season, h); + insert_sprite_proxy(&mr->Terrain_Buldings_Radar, &set->Terrain_Buldings_Radar, season, h); + insert_sprite_proxies(mr->Flood_Plains_Images, set->Flood_Plains_Images, season, h, 16); + insert_sprite_proxies(mr->Polar_Icecaps_Images, set->Polar_Icecaps_Images, season, h, 32); + insert_sprite_proxies(mr->Roads_Images, set->Roads_Images, season, h, 256); + insert_sprite_proxies(mr->Railroads_Images, set->Railroads_Images, season, h, 272); + insert_sprite_proxies(mr->Terrain_Buldings_Airfields, set->Terrain_Buldings_Airfields, season, h, 2); + insert_sprite_proxies(mr->Terrain_Buldings_Camp, set->Terrain_Buldings_Camp, season, h, 4); + insert_sprite_proxies(mr->Terrain_Buldings_Fortress, set->Terrain_Buldings_Fortress, season, h, 4); + insert_sprite_proxies(mr->Terrain_Buldings_Barricade, set->Terrain_Buldings_Barricade, season, h, 4); + insert_sprite_proxies(mr->Goody_Huts_Images, set->Goody_Huts_Images, season, h, 8); + insert_sprite_proxies(mr->Terrain_Buldings_Outposts, set->Terrain_Buldings_Outposts, season, h, 3); + insert_sprite_proxies(mr->Pollution, set->Pollution, season, h, 25); + insert_sprite_proxies(mr->Craters, set->Craters, season, h, 25); + insert_sprite_proxies(mr->Tnt_Images, set->Tnt_Images, season, h, 18); + insert_sprite_proxies(mr->Waterfalls_Images, set->Waterfalls_Images, season, h, 4); + insert_sprite_proxies(mr->LM_Terrain, set->LM_Terrain, season, h, 7); + insert_sprite_proxies(mr->Marsh_Large, set->Marsh_Large, season, h, 8); + insert_sprite_proxies(mr->Marsh_Small, set->Marsh_Small, season, h, 10); + insert_sprite_proxies(mr->Volcanos_Images, set->Volcanos_Images, season, h, 16); + insert_sprite_proxies(mr->Volcanos_Forests_Images, set->Volcanos_Forests_Images, season, h, 16); + insert_sprite_proxies(mr->Volcanos_Jungles_Images, set->Volcanos_Jungles_Images, season, h, 16); + insert_sprite_proxies(mr->Volcanos_Snow_Images, set->Volcanos_Snow_Images, season, h, 16); + insert_sprite_proxies(mr->Grassland_Forests_Large, set->Grassland_Forests_Large, season, h, 8); + insert_sprite_proxies(mr->Plains_Forests_Large, set->Plains_Forests_Large, season, h, 8); + insert_sprite_proxies(mr->Tundra_Forests_Large, set->Tundra_Forests_Large, season, h, 8); + insert_sprite_proxies(mr->Grassland_Forests_Small, set->Grassland_Forests_Small, season, h, 10); + insert_sprite_proxies(mr->Plains_Forests_Small, set->Plains_Forests_Small, season, h, 10); + insert_sprite_proxies(mr->Tundra_Forests_Small, set->Tundra_Forests_Small, season, h, 10); + insert_sprite_proxies(mr->Grassland_Forests_Pines, set->Grassland_Forests_Pines, season, h, 12); + insert_sprite_proxies(mr->Plains_Forests_Pines, set->Plains_Forests_Pines, season, h, 12); + insert_sprite_proxies(mr->Tundra_Forests_Pines, set->Tundra_Forests_Pines, season, h, 12); + insert_sprite_proxies(mr->Irrigation_Desert_Images, set->Irrigation_Desert_Images, season, h, 16); + insert_sprite_proxies(mr->Irrigation_Plains_Images, set->Irrigation_Plains_Images, season, h, 16); + insert_sprite_proxies(mr->Irrigation_Images, set->Irrigation_Images, season, h, 16); + insert_sprite_proxies(mr->Irrigation_Tundra_Images, set->Irrigation_Tundra_Images, season, h, 16); + insert_sprite_proxies(mr->Grassland_Jungles_Large, set->Grassland_Jungles_Large, season, h, 8); + insert_sprite_proxies(mr->Grassland_Jungles_Small, set->Grassland_Jungles_Small, season, h, 12); + insert_sprite_proxies(mr->Mountains_Images, set->Mountains_Images, season, h, 16); + insert_sprite_proxies(mr->Mountains_Forests_Images, set->Mountains_Forests_Images, season, h, 16); + insert_sprite_proxies(mr->Mountains_Jungles_Images, set->Mountains_Jungles_Images, season, h, 16); + insert_sprite_proxies(mr->Mountains_Snow_Images, set->Mountains_Snow_Images, season, h, 16); + insert_sprite_proxies(mr->Hills_Images, set->Hills_Images, season, h, 16); + insert_sprite_proxies(mr->Hills_Forests_Images, set->Hills_Forests_Images, season, h, 16); + insert_sprite_proxies(mr->Hills_Jungle_Images, set->Hills_Jungle_Images, season, h, 16); + insert_sprite_proxies(mr->Delta_Rivers_Images, set->Delta_Rivers_Images, season, h, 16); + insert_sprite_proxies(mr->Mountain_Rivers_Images, set->Mountain_Rivers_Images, season, h, 16); + insert_sprite_proxies(mr->LM_Mountains_Images, set->LM_Mountains_Images, season, h, 16); + insert_sprite_proxies(mr->LM_Forests_Large_Images, set->LM_Forests_Large_Images, season, h, 8); + insert_sprite_proxies(mr->LM_Forests_Small_Images, set->LM_Forests_Small_Images, season, h, 10); + insert_sprite_proxies(mr->LM_Forests_Pines_Images, set->LM_Forests_Pines_Images, season, h, 12); + insert_sprite_proxies(mr->LM_Hills_Images, set->LM_Hills_Images, season, h, 16); + + if (is->current_config.enable_districts) { + for (int dc = 0; dc < is->district_count; dc++) { + struct district_config const * cfg = &is->district_configs[dc]; + int variant_capacity = ARRAY_LEN (is->district_img_sets[dc].imgs); + int variant_count = cfg->img_path_count; + if (variant_count <= 0) continue; - for (int era = 0; era < era_count; era++) { - for (int col = 0; col < column_count; col++) { - Sprite * base = &is->district_img_sets[dc].imgs[variant_i][era][col]; - Sprite * proxy = &is->day_night_cycle_imgs[h].District_Images[dc][variant_i][era][col]; - insert_sprite_proxy (base, proxy, h); + if (variant_count > variant_capacity) + variant_count = variant_capacity; + + int era_count = cfg->vary_img_by_era ? 4 : 1; + int column_count = cfg->img_column_count; + + for (int variant_i = 0; variant_i < variant_count; variant_i++) { + if ((cfg->img_paths[variant_i] == NULL) || (cfg->img_paths[variant_i][0] == '\0')) + continue; + for (int era = 0; era < era_count; era++) { + for (int col = 0; col < column_count; col++) { + Sprite * base = &is->district_img_sets[dc].imgs[variant_i][era][col]; + Sprite * proxy = &set->District_Images[dc][variant_i][era][col]; + insert_sprite_proxy (base, proxy, season, h); + } } } } - } - insert_sprite_proxy (&is->abandoned_district_img, &is->day_night_cycle_imgs[h].Abandoned_District_Image, h); - insert_sprite_proxy (&is->abandoned_maritime_district_img, &is->day_night_cycle_imgs[h].Abandoned_Maritime_District_Image, h); + insert_sprite_proxy (&is->abandoned_district_img, &set->Abandoned_District_Image, season, h); + insert_sprite_proxy (&is->abandoned_maritime_district_img, &set->Abandoned_Maritime_District_Image, season, h); - // Wonder districts - if (is->current_config.enable_wonder_districts) { - for (int wi = 0; wi < is->wonder_district_count; wi++) { - Sprite * base_img = &is->wonder_district_img_sets[wi].img; - Sprite * proxy_img = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].img; - insert_sprite_proxy (base_img, proxy_img, h); - - Sprite * base_construct = &is->wonder_district_img_sets[wi].construct_img; - Sprite * proxy_construct = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].construct_img; - insert_sprite_proxy (base_construct, proxy_construct, h); - - if (is->wonder_district_img_sets[wi].alt_dir_img.vtable != NULL) { - Sprite * base_alt = &is->wonder_district_img_sets[wi].alt_dir_img; - Sprite * proxy_alt = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].alt_dir_img; - insert_sprite_proxy (base_alt, proxy_alt, h); - } + if (is->current_config.enable_wonder_districts) { + for (int wi = 0; wi < is->wonder_district_count; wi++) { + insert_sprite_proxy (&is->wonder_district_img_sets[wi].img, &set->Wonder_District_Images[wi].img, season, h); + insert_sprite_proxy (&is->wonder_district_img_sets[wi].construct_img, &set->Wonder_District_Images[wi].construct_img, season, h); - if (is->wonder_district_img_sets[wi].alt_dir_construct_img.vtable != NULL) { - Sprite * base_alt_construct = &is->wonder_district_img_sets[wi].alt_dir_construct_img; - Sprite * proxy_alt_construct = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].alt_dir_construct_img; - insert_sprite_proxy (base_alt_construct, proxy_alt_construct, h); + if (is->wonder_district_img_sets[wi].alt_dir_img.vtable != NULL) + insert_sprite_proxy (&is->wonder_district_img_sets[wi].alt_dir_img, &set->Wonder_District_Images[wi].alt_dir_img, season, h); + if (is->wonder_district_img_sets[wi].alt_dir_construct_img.vtable != NULL) + insert_sprite_proxy (&is->wonder_district_img_sets[wi].alt_dir_construct_img, &set->Wonder_District_Images[wi].alt_dir_construct_img, season, h); } } } - } - // Natural wonders - if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { - for (int ni = 0; ni < is->natural_wonder_count; ni++) { - Sprite * base_nw = &is->natural_wonder_img_sets[ni].img; - Sprite * proxy_nw = &is->day_night_cycle_imgs[h].Natural_Wonder_Images[ni].img; - insert_sprite_proxy (base_nw, proxy_nw, h); + if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { + for (int ni = 0; ni < is->natural_wonder_count; ni++) + insert_sprite_proxy (&is->natural_wonder_img_sets[ni].img, &set->Natural_Wonder_Images[ni].img, season, h); } } } @@ -17481,10 +17933,12 @@ build_sprite_proxies_24(Map_Renderer *mr) { } void -init_day_night_images() +init_day_night_and_seasonal_images() { if (is->day_night_cycle_img_state != IS_UNINITED) return; + if (is->cycle_imgs == NULL) + return; const char *hour_strs[24] = { "2400", "0100", "0200", "0300", "0400", "0500", "0600", "0700", @@ -17492,26 +17946,34 @@ init_day_night_images() "1600", "1700", "1800", "1900", "2000", "2100", "2200", "2300" }; - for (int i = 0; i < 24; i++) { + int required_season_mask = get_required_season_mask_for_cycle_loading (); + int required_hour_mask = get_required_hour_mask_for_cycle_loading (); + for (int season = 0; season < COUNT_CYCLE_SEASONS; season++) { + if ((required_season_mask & (1 << season)) == 0) + continue; + for (int i = 0; i < 24; i++) { + if ((required_hour_mask & (1 << i)) == 0) + continue; - char art_dir[200]; - char temp_path[2*MAX_PATH]; - snprintf (art_dir, sizeof art_dir, "DayNight/%s", hour_strs[i]); - get_mod_art_path (art_dir, temp_path, sizeof temp_path); - bool success = load_day_night_hour_images (&is->day_night_cycle_imgs[i], temp_path, hour_strs[i]); + char art_dir[200]; + char temp_path[2*MAX_PATH]; + snprintf (art_dir, sizeof art_dir, "DayNight/%s/%s", cycle_season_names[season], hour_strs[i]); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); + bool success = load_day_night_hour_and_season_images (&is->cycle_imgs[24 * season + i], temp_path, cycle_season_names[season], hour_strs[i]); - if (!success) { - char ss[200]; - snprintf(ss, sizeof ss, "Failed to load day/night cycle images for hour %s, reverting to base game art.", hour_strs[i]); - pop_up_in_game_error (ss); + if (!success) { + char ss[300]; + snprintf (ss, sizeof ss, "Failed to load day/night cycle images for season %s at hour %s, reverting to base game art.", cycle_season_names[season], hour_strs[i]); + pop_up_in_game_error (ss); - is->day_night_cycle_img_state = IS_INIT_FAILED; - return; + is->day_night_cycle_img_state = IS_INIT_FAILED; + return; + } } } Map_Renderer * mr = &p_bic_data->Map.Renderer; - build_sprite_proxies_24(mr); + build_sprite_proxies(mr); is->day_night_cycle_img_state = IS_OK; } @@ -17519,15 +17981,104 @@ init_day_night_images() void deindex_day_night_image_proxies() { - if (!is->day_night_cycle_img_proxies_indexed) + if (!is->day_night_cycle_img_proxies_indexed || is->day_night_sprite_proxy_by_season_and_hour == NULL) return; - for (int i = 0; i < 24; i++) { - table_deinit (&is->day_night_sprite_proxy_by_hour[i]); - } + for (int season = 0; season < COUNT_CYCLE_SEASONS; season++) + for (int i = 0; i < 24; i++) + table_deinit (&is->day_night_sprite_proxy_by_season_and_hour[24 * season + i]); is->day_night_cycle_img_proxies_indexed = false; } +int +calculate_current_seasonal_cycle (bool transition_on_day_night_hour_hit) +{ + int output = CS_SUMMER; + int enabled_mask = normalize_enabled_season_mask (is->current_config.enabled_seasons_mask); + switch (is->current_config.seasonal_cycle_mode) { + case SCM_OFF: + return CS_SUMMER; + + case SCM_TIMER: { + output = get_first_enabled_season (enabled_mask); + if (is->seasonal_cycle_unstarted) { + is->current_seasonal_cycle = output; + QueryPerformanceCounter (&is->last_seasonal_cycle_update_time); + } + + LARGE_INTEGER perf_freq, time_now; + QueryPerformanceFrequency (&perf_freq); + QueryPerformanceCounter (&time_now); + + double elapsed_seconds = + (double)(time_now.QuadPart - is->last_seasonal_cycle_update_time.QuadPart) / + (double)perf_freq.QuadPart; + if (elapsed_seconds > (double)is->current_config.elapsed_minutes_per_season_transition * 60.0) { + output = get_next_enabled_season (is->current_seasonal_cycle, enabled_mask); + is->last_seasonal_cycle_update_time = time_now; + } else + output = is->current_seasonal_cycle; + break; + } + + case SCM_USER_SEASON: { + output = get_current_local_season (); + if ((enabled_mask & (1 << output)) == 0) + output = get_first_enabled_season (enabled_mask); + break; + } + + case SCM_EVERY_TURN: { + output = get_first_enabled_season (enabled_mask); + if (is->seasonal_cycle_unstarted) { + is->current_seasonal_cycle = output; + is->turns_in_current_season = 0; + break; + } + if ((enabled_mask & (1 << is->current_seasonal_cycle)) == 0) { + is->current_seasonal_cycle = output; + is->turns_in_current_season = 0; + break; + } + int turns_per_season = not_below (1, is->current_config.fixed_turns_per_season); + is->turns_in_current_season += 1; + if (is->turns_in_current_season >= turns_per_season) { + is->turns_in_current_season = 0; + output = get_next_enabled_season (is->current_seasonal_cycle, enabled_mask); + } else + output = is->current_seasonal_cycle; + break; + } + + case SCM_ON_DAY_NIGHT_HOUR: { + output = get_first_enabled_season (enabled_mask); + if (is->seasonal_cycle_unstarted) { + is->current_seasonal_cycle = output; + break; + } + if ((enabled_mask & (1 << is->current_seasonal_cycle)) == 0) { + is->current_seasonal_cycle = output; + break; + } + if (transition_on_day_night_hour_hit) + output = get_next_enabled_season (is->current_seasonal_cycle, enabled_mask); + else + output = is->current_seasonal_cycle; + break; + } + + case SCM_SPECIFIED: { + int pinned = clamp (CS_SUMMER, CS_SPRING, is->current_config.pinned_season_for_seasonal_cycle); + output = ((enabled_mask & (1 << pinned)) != 0) ? pinned : get_first_enabled_season (enabled_mask); + break; + } + } + + output = clamp (CS_SUMMER, CS_SPRING, output); + is->seasonal_cycle_unstarted = false; + return output; +} + int calculate_current_day_night_cycle_hour () { @@ -17607,19 +18158,29 @@ patch_Map_Renderer_load_images (Map_Renderer *this, int edx) { Map_Renderer_load_images(this, __); - // Initialize day/night cycle and re-calculate hour, if applicable - if (is->current_config.day_night_cycle_mode != DNCM_OFF) { - is->current_day_night_cycle = calculate_current_day_night_cycle_hour (); + if (is->current_config.day_night_cycle_mode != DNCM_OFF || is->current_config.seasonal_cycle_mode != SCM_OFF) { + if (! allocate_day_night_cycle_runtime_storage ()) { + is->day_night_cycle_img_state = IS_INIT_FAILED; + return; + } + + if (is->current_config.day_night_cycle_mode != DNCM_OFF) + is->current_day_night_cycle = calculate_current_day_night_cycle_hour (); + if ((is->current_config.seasonal_cycle_mode != SCM_OFF) && + (is->seasonal_cycle_unstarted || + ((is->current_config.seasonal_cycle_mode != SCM_EVERY_TURN) && + (is->current_config.seasonal_cycle_mode != SCM_ON_DAY_NIGHT_HOUR)))) + is->current_seasonal_cycle = calculate_current_seasonal_cycle (false); if (is->day_night_cycle_img_state == IS_UNINITED) { - init_day_night_images (); + init_day_night_and_seasonal_images (); } if (is->day_night_cycle_img_state == IS_OK) { // Sprite proxies are deindexed during each load event as sprite instances (really only Resources, which are reloaded) may change. if (!is->day_night_cycle_img_proxies_indexed) { - build_sprite_proxies_24(this); + build_sprite_proxies(this); } } } @@ -17780,6 +18341,7 @@ patch_init_floating_point () {"enable_wonder_districts" , false, offsetof (struct c3x_config, enable_wonder_districts)}, {"enable_natural_wonders" , false, offsetof (struct c3x_config, enable_natural_wonders)}, {"add_natural_wonders_to_scenarios_if_none" , false, offsetof (struct c3x_config, add_natural_wonders_to_scenarios_if_none)}, + {"enable_custom_animations" , false, offsetof (struct c3x_config, enable_custom_animations)}, {"enable_named_tiles" , false, offsetof (struct c3x_config, enable_named_tiles)}, {"enable_distribution_hub_districts" , false, offsetof (struct c3x_config, enable_distribution_hub_districts)}, {"enable_aerodrome_districts" , false, offsetof (struct c3x_config, enable_aerodrome_districts)}, @@ -17842,10 +18404,14 @@ patch_init_floating_point () {"elapsed_minutes_per_day_night_hour_transition" , 3, offsetof (struct c3x_config, elapsed_minutes_per_day_night_hour_transition)}, {"fixed_hours_per_turn_for_day_night_cycle" , 1, offsetof (struct c3x_config, fixed_hours_per_turn_for_day_night_cycle)}, {"pinned_hour_for_day_night_cycle" , 0, offsetof (struct c3x_config, pinned_hour_for_day_night_cycle)}, + {"show_tile_destruction_animation_for_turns" , 2, offsetof (struct c3x_config, show_tile_destruction_animation_for_turns)}, + {"elapsed_minutes_per_season_transition" , 3, offsetof (struct c3x_config, elapsed_minutes_per_season_transition)}, + {"fixed_turns_per_season" , 3, offsetof (struct c3x_config, fixed_turns_per_season)}, + {"transition_season_on_day_night_hour" , 0, offsetof (struct c3x_config, transition_season_on_day_night_hour)}, {"years_to_double_building_culture" , 1000, offsetof (struct c3x_config, years_to_double_building_culture)}, {"tourism_time_scale_percent" , 100, offsetof (struct c3x_config, tourism_time_scale_percent)}, - {"luxury_randomized_appearance_rate_percent" , 100, offsetof (struct c3x_config, luxury_randomized_appearance_rate_percent)}, - {"tiles_per_non_luxury_resource" , 32, offsetof (struct c3x_config, tiles_per_non_luxury_resource)}, + {"luxury_randomized_appearance_rate_percent" , 100, offsetof (struct c3x_config, luxury_randomized_appearance_rate_percent)}, + {"tiles_per_non_luxury_resource" , 32, offsetof (struct c3x_config, tiles_per_non_luxury_resource)}, {"special_capital_decorruption_effect" , 10, offsetof (struct c3x_config, special_capital_decorruption_effect)}, {"city_limit" , 2048, offsetof (struct c3x_config, city_limit)}, {"maximum_pop_before_neighborhood_needed" , 8, offsetof (struct c3x_config, maximum_pop_before_neighborhood_needed)}, @@ -17931,10 +18497,14 @@ patch_init_floating_point () base_config.unit_cycle_search_criteria = UCSC_STANDARD; base_config.city_work_radius = 2; base_config.day_night_cycle_mode = DNCM_OFF; + base_config.seasonal_cycle_mode = SCM_OFF; + base_config.enabled_seasons_mask = 0xF; + base_config.pinned_season_for_seasonal_cycle = CS_SUMMER; base_config.distribution_hub_yield_division_mode = DHYDM_FLAT; base_config.ai_distribution_hub_build_strategy = ADHBS_BY_CITY_COUNT; base_config.ai_auto_build_great_wall_strategy = AAGWS_ALL_BORDERS; base_config.great_wall_auto_build_wonder_improv_id = -1; + base_config.show_tile_destruct_animation_after = TDAT_BOMBARD | TDAT_PILLAGE | TDAT_BOMB; for (int n = 0; n < ARRAY_LEN (boolean_config_options); n++) *((char *)&base_config + boolean_config_options[n].offset) = boolean_config_options[n].base_val; for (int n = 0; n < ARRAY_LEN (integer_config_options); n++) @@ -18134,6 +18704,22 @@ patch_init_floating_point () is->accessing_save_file = NULL; is->drawn_strat_resource_count = 0; + is->current_day_night_cycle = 12; + is->day_night_cycle_unstarted = true; + is->current_seasonal_cycle = CS_SUMMER; + is->seasonal_cycle_unstarted = true; + is->turns_in_current_season = 0; + is->day_night_cycle_img_proxies_indexed = false; + is->day_night_sprite_proxy_by_season_and_hour = NULL; + is->cycle_imgs = NULL; + is->tile_destruct_animation_ages = NULL; + is->tile_animation_pcx_sprite_lookup = (struct table) {0}; + is->tile_animation_pcx_rule_key_to_index = (struct table) {0}; + is->tile_animation_pcx_rule_key_count = 0; + memset (is->tile_animation_pcx_rule_masks, 0, sizeof is->tile_animation_pcx_rule_masks); + memset (is->tile_animation_pcx_word_mask, 0, sizeof is->tile_animation_pcx_word_mask); + memset (is->tile_animation_pcx_active_word_mask, 0, sizeof is->tile_animation_pcx_active_word_mask); + is->tile_animation_has_pcx_rules = false; is->charmed_types_converted_to_ptw_arty = NULL; is->count_charmed_types_converted_to_ptw_arty = 0; @@ -18592,16 +19178,20 @@ patch_Unit_bombard_tile (Unit * this, int edx, int x, int y) { Tile * target_tile = NULL; bool had_district_before = false; + unsigned int overlays_before = 0; int tile_x = x; int tile_y = y; struct district_instance * inst; - if (is->current_config.enable_districts) { + if (is->current_config.enable_districts || is->current_config.enable_custom_animations) { wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); target_tile = tile_at (tile_x, tile_y); if ((target_tile != NULL) && (target_tile != p_null_tile)) { - inst = get_district_instance (target_tile); - had_district_before = (inst != NULL); + overlays_before = target_tile->vtable->m42_Get_Overlays (target_tile, __, 0); + if (is->current_config.enable_districts) { + inst = get_district_instance (target_tile); + had_district_before = (inst != NULL); + } } } @@ -18616,6 +19206,13 @@ patch_Unit_bombard_tile (Unit * this, int edx, int x, int y) if ((overlays & TILE_FLAG_MINE) == 0) handle_district_destroyed_by_attack (target_tile, tile_x, tile_y, false); } + if ((target_tile != NULL) && (target_tile != p_null_tile)) { + unsigned int overlays_after = target_tile->vtable->m42_Get_Overlays (target_tile, __, 0); + if (overlays_after != overlays_before) { + int trigger = (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air) ? TDAT_BOMB : TDAT_BOMBARD; + trigger_tile_destruct_animation (tile_x, tile_y, trigger); + } + } } void __fastcall @@ -21277,6 +21874,7 @@ patch_load_scenario (BIC * this, int edx, char * param_1, unsigned * param_2) is->destroy_tnx_cache (is->tnx_cache); is->tnx_cache = NULL; } + reset_tile_animation_runtime_state (); unsigned tr = load_scenario (this, __, param_1, param_2); char * scenario_path = param_1; @@ -21304,6 +21902,7 @@ patch_load_scenario (BIC * this, int edx, char * param_1, unsigned * param_2) reset_district_state (true); load_districts_config (); } + load_tile_animation_configs (); // Initialize Trade Net X if (is->current_config.enable_trade_net_x && (is->tnx_init_state == IS_UNINITED)) { @@ -21533,13 +22132,30 @@ patch_load_scenario (BIC * this, int edx, char * param_1, unsigned * param_2) else if (is->current_config.override_no_ai_patrol == NAPO_NONE) *p_allow_ai_patrol = 0 == get_int_from_conquests_ini ("NoAIPatrol", 1, 0); - // Clear day/night cycle vars and deindex sprite proxies, if necessary. - if (is->current_config.day_night_cycle_mode != DNCM_OFF) { - is->day_night_cycle_unstarted = true; - is->current_day_night_cycle = 12; - if (is->day_night_cycle_img_proxies_indexed) { - deindex_day_night_image_proxies (); + // Reset cycle vars and clear any indexed sprite proxies. + is->day_night_cycle_unstarted = true; + is->current_day_night_cycle = 12; + is->seasonal_cycle_unstarted = true; + is->current_seasonal_cycle = CS_SUMMER; + is->turns_in_current_season = 0; + if (is->day_night_cycle_img_proxies_indexed) + deindex_day_night_image_proxies (); + if ((is->current_config.day_night_cycle_mode != DNCM_OFF) || + (is->current_config.seasonal_cycle_mode != SCM_OFF)) { + if (! allocate_day_night_cycle_runtime_storage ()) { + is->day_night_cycle_img_state = IS_INIT_FAILED; + pop_up_in_game_error ("Could not allocate memory for day/night cycle sprite proxies."); + } + } else { + if (is->day_night_sprite_proxy_by_season_and_hour != NULL) { + free (is->day_night_sprite_proxy_by_season_and_hour); + is->day_night_sprite_proxy_by_season_and_hour = NULL; } + if ((is->cycle_imgs != NULL) && (is->day_night_cycle_img_state != IS_OK)) { + free (is->cycle_imgs); + is->cycle_imgs = NULL; + } + is->day_night_cycle_img_state = IS_UNINITED; } return tr; @@ -23712,14 +24328,16 @@ patch_City_compute_corrupted_yield (City * this, int edx, int gross_yield, bool int __fastcall patch_Sprite_draw (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - Sprite * to_draw = get_sprite_proxy_for_current_hour(this); + Sprite * to_draw = get_cycle_sprite_proxy(this); return Sprite_draw(to_draw ? to_draw : this, __, canvas, pixel_x, pixel_y, color_table); } int __fastcall patch_Sprite_draw_on_map (Sprite * this, int edx, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int param_4, int param_5, int param_6, int param_7) { - Sprite * to_draw = get_sprite_proxy_for_current_hour(this); + if (is->current_config.enable_custom_animations) + register_tile_animation_pcx_draw_for_current_tile (this); + Sprite * to_draw = get_cycle_sprite_proxy(this); return Sprite_draw_on_map(to_draw ? to_draw : this, __, map_renderer, pixel_x, pixel_y, param_4, param_5, param_6, param_7); } @@ -24928,6 +25546,16 @@ patch_Map_Renderer_m71_Draw_Tiles (Map_Renderer * this, int edx, int param_1, in is->saved_tile_count = -1; } + if (is->current_config.enable_custom_animations && is->tile_animation_has_pcx_rules) { + if (is->tile_animation_pcx_sprite_lookup.len == 0) + rebuild_tile_animation_pcx_sprite_lookup (); + if (tile_animation_cache_needs_rebuild ()) + rebuild_tile_animation_rule_match_cache (); + // Per-frame refresh: clear stale PCX bits and rebuild only time-valid PCX candidates. + refresh_tile_animation_pcx_active_mask (); + clear_tile_animation_pcx_matches_in_cache (); + } + Map_Renderer_m71_Draw_Tiles (this, __, param_1, param_2, param_3); } @@ -25598,6 +26226,8 @@ patch_Map_impl_generate (Map * this, int edx, int seed, bool is_multiplayer_game if (is->current_config.enable_natural_wonders) place_natural_wonders_on_map (); + if (is->current_config.enable_custom_animations) + reset_tile_animation_runtime_state (); } int __fastcall @@ -26476,13 +27106,18 @@ patch_show_intro_after_load_popup (void * this, int edx, int param_1, int param_ void __fastcall patch_Trade_Net_recompute_city_connections (Trade_Net * this, int edx, int civ_id, bool redo_road_network, byte param_3, int redo_roads_for_city_id); -void * __cdecl -patch_do_load_game (char * param_1) -{ - void * tr = do_load_game (param_1); - - if (is->current_config.restore_unit_directions_on_game_load && (p_units->Units != NULL)) - for (int n = 0; n <= p_units->LastIndex; n++) { +void * __cdecl +patch_do_load_game (char * param_1) +{ + void * tr = do_load_game (param_1); + free_tile_animation_selected_matrix (); + clear_tile_animation_pcx_sprite_lookup (); + refresh_tile_animation_pcx_rule_mask (); + is->tile_animation_spawn_effect_override = 0; + is->tile_animation_spawn_effect_override_active = false; + + if (is->current_config.restore_unit_directions_on_game_load && (p_units->Units != NULL)) + for (int n = 0; n <= p_units->LastIndex; n++) { Unit * unit = get_unit_ptr (n); if ((unit != NULL) && (unit->Body.UnitState != UnitState_Fortifying)) { if (Map_in_range (&p_bic_data->Map, __, unit->Body.X, unit->Body.Y) && @@ -26616,14 +27251,33 @@ patch_perform_interturn_in_main_loop () perform_interturn (); - if (is->current_config.day_night_cycle_mode) { - if (is->day_night_cycle_img_state == IS_OK) { - int new_hour = calculate_current_day_night_cycle_hour (); + if (is->day_night_cycle_img_state == IS_OK) { + bool redraw = false; + int old_hour = is->current_day_night_cycle; + int new_hour = old_hour; + if (is->current_config.day_night_cycle_mode != DNCM_OFF) { + new_hour = calculate_current_day_night_cycle_hour (); if (new_hour != is->current_day_night_cycle) { is->current_day_night_cycle = new_hour; - p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + redraw = true; + } + } + if (is->current_config.seasonal_cycle_mode != SCM_OFF) { + bool transition_on_day_night_hour_hit = false; + if (is->current_config.seasonal_cycle_mode == SCM_ON_DAY_NIGHT_HOUR) { + int transition_hour = clamp (0, 23, is->current_config.transition_season_on_day_night_hour); + transition_on_day_night_hour_hit = (is->current_config.day_night_cycle_mode != DNCM_OFF) && + (new_hour != old_hour) && (new_hour == transition_hour); + } + int old_season = is->current_seasonal_cycle; + int new_season = calculate_current_seasonal_cycle (transition_on_day_night_hour_hit); + if (new_season != old_season) { + is->current_seasonal_cycle = new_season; + redraw = true; } } + if (redraw) + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); } if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { @@ -26636,6 +27290,10 @@ patch_perform_interturn_in_main_loop () is->city_loc_display_perspective = -1; p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); // Trigger map redraw } + if (is->current_config.enable_custom_animations) { + age_tile_destruct_animations (); + rebuild_tile_animation_rule_match_cache (); + } if (is->current_config.measure_turn_times) { long long ts_after; @@ -27719,6 +28377,16 @@ patch_Unit_move_to_adjacent_tile (Unit * this, int edx, int neighbor_index, bool is->move_spend_override_unit = NULL; is->move_spend_override_value = 0; + bool redraw_after_move = false; + if (is->current_config.enable_custom_animations) { + FOR_TILES_AROUND (tai, 21, this->Body.X, this->Body.Y) { + if (tile_has_matching_resource_animation_for_draw (tai.tile, tai.tile_x, tai.tile_y)) { + redraw_after_move = true; + break; + } + } + } + bool const allow_worker_coast = is->current_config.enable_districts && is->current_config.workers_can_enter_coast && is_worker (this); bool const allow_bridge_walk = is->current_config.enable_districts && is->current_config.enable_bridge_districts && @@ -27819,6 +28487,21 @@ patch_Unit_move_to_adjacent_tile (Unit * this, int edx, int neighbor_index, bool } } + if (is->current_config.enable_custom_animations) { + FOR_TILES_AROUND (tai, 21, this->Body.X, this->Body.Y) { + if (tile_has_matching_resource_animation_for_draw (tai.tile, tai.tile_x, tai.tile_y)) { + redraw_after_move = true; + break; + } + } + } + + // We have to call the draw method directly since the game doesn't necessarily fully redraw newly (un)revealed + // tiles in all cases. This avoids situations where a newly revealed tile shows both the static resource overlay + // and the animated one on top of it during a turn. + if (redraw_after_move) + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + is->temporarily_disallow_lethal_zoc = false; is->moving_unit_to_adjacent_tile = false; is->move_spend_override_unit = NULL; @@ -28082,10 +28765,11 @@ patch_Unit_attack_tile (Unit * this, int edx, int x, int y, int bombarding) Tile * target_tile = NULL; bool had_district_before = false; int district_id_before = -1; + unsigned int overlays_before = 0; int tile_x = x; int tile_y = y; - if (is->current_config.enable_districts) { + if (is->current_config.enable_districts || is->current_config.enable_custom_animations) { // Check if this is a completed wonder district that cannot be destroyed if (is->current_config.enable_wonder_districts && @@ -28110,10 +28794,13 @@ patch_Unit_attack_tile (Unit * this, int edx, int x, int y, int bombarding) wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); target_tile = tile_at (tile_x, tile_y); if ((target_tile != NULL) && (target_tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (target_tile); - if (inst != NULL) { - had_district_before = true; - district_id_before = inst->district_id; + overlays_before = target_tile->vtable->m42_Get_Overlays (target_tile, __, 0); + if (is->current_config.enable_districts) { + struct district_instance * inst = get_district_instance (target_tile); + if (inst != NULL) { + had_district_before = true; + district_id_before = inst->district_id; + } } } } @@ -28137,6 +28824,17 @@ patch_Unit_attack_tile (Unit * this, int edx, int x, int y, int bombarding) handle_district_destroyed_by_attack (target_tile, tile_x, tile_y, ! is_water_tile); } } + if ((target_tile != NULL) && (target_tile != p_null_tile)) { + unsigned int overlays_after = target_tile->vtable->m42_Get_Overlays (target_tile, __, 0); + if (overlays_after != overlays_before) { + if ((! bombarding) && (tile_x == this->Body.X) && (tile_y == this->Body.Y)) + trigger_tile_destruct_animation (tile_x, tile_y, TDAT_PILLAGE); + else if (bombarding) { + int trigger = (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air) ? TDAT_BOMB : TDAT_BOMBARD; + trigger_tile_destruct_animation (tile_x, tile_y, trigger); + } + } + } is->unit_bombard_attacking_tile = NULL; is->attacking_tile_x = is->attacking_tile_y = -1; @@ -29842,6 +30540,34 @@ patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR fi serialize_aligned_text ("current_day_night_cycle", &mod_data); int_to_bytes (buffer_allocate (&mod_data, sizeof is->current_day_night_cycle), is->current_day_night_cycle); } + if (is->current_config.seasonal_cycle_mode != SCM_OFF) { + serialize_aligned_text ("current_seasonal_cycle", &mod_data); + int_to_bytes (buffer_allocate (&mod_data, sizeof is->current_seasonal_cycle), is->current_seasonal_cycle); + serialize_aligned_text ("turns_in_current_season", &mod_data); + int_to_bytes (buffer_allocate (&mod_data, sizeof is->turns_in_current_season), is->turns_in_current_season); + } + if (is->current_config.enable_custom_animations && (is->tile_destruct_animation_ages != NULL)) { + int entry_count = 0; + for (int tile_index = 0; tile_index < p_bic_data->Map.TileCount; tile_index++) + if (is->tile_destruct_animation_ages[tile_index] > 0) + entry_count++; + if (entry_count > 0) { + serialize_aligned_text ("tile_destruct_animation_ages", &mod_data); + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_count)); + int * out = chunk + 1; + chunk[0] = entry_count; + for (int tile_index = 0; tile_index < p_bic_data->Map.TileCount; tile_index++) { + if (is->tile_destruct_animation_ages[tile_index] == 0) + continue; + int tile_x, tile_y; + tile_index_to_coords (&p_bic_data->Map, tile_index, &tile_x, &tile_y); + out[0] = tile_x; + out[1] = tile_y; + out[2] = is->tile_destruct_animation_ages[tile_index]; + out += 3; + } + } + } if (is->current_config.enable_districts && (is->district_count > 0)) { serialize_aligned_text ("district_config_names", &mod_data); int * entry_count = (int *)buffer_allocate (&mod_data, sizeof(int)); @@ -30181,6 +30907,8 @@ int __cdecl patch_move_game_data (byte * buffer, bool save_else_load) { int tr = move_game_data (buffer, save_else_load); + if (! save_else_load && is->current_config.enable_custom_animations) + reset_tile_animation_runtime_state (); if (! save_else_load) { // Free all district_instance structs first @@ -30333,14 +31061,52 @@ patch_move_game_data (byte * buffer, bool save_else_load) // only the standard graphics; I didn't test). If day/night cycle mode is active, restore the proxies now if they // haven't already been. if ((is->day_night_cycle_img_state == IS_OK) && ! is->day_night_cycle_img_proxies_indexed) - build_sprite_proxies_24 (&p_bic_data->Map.Renderer); + build_sprite_proxies (&p_bic_data->Map.Renderer); // Because we've restored current_day_night_cycle from the save, set that is is not the first turn so the cycle - // doesn't get restarted. - is->day_night_cycle_unstarted = false; - - // ToC-3 - } else if (match_save_chunk_name (&cursor, "great_wall_auto_build_state")) { + // doesn't get restarted. + is->day_night_cycle_unstarted = false; + + } else if (match_save_chunk_name (&cursor, "current_seasonal_cycle")) { + is->current_seasonal_cycle = clamp (CS_SUMMER, CS_SPRING, *((int *)cursor)++); + is->seasonal_cycle_unstarted = false; + if ((is->day_night_cycle_img_state == IS_OK) && ! is->day_night_cycle_img_proxies_indexed) + build_sprite_proxies (&p_bic_data->Map.Renderer); + + } else if (match_save_chunk_name (&cursor, "turns_in_current_season")) { + is->turns_in_current_season = not_below (0, *((int *)cursor)++); + + } else if (match_save_chunk_name (&cursor, "tile_destruct_animation_ages")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int entry_count = *((int *)cursor)++; + remaining_bytes -= sizeof(int); + if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { + ensure_tile_destruct_animation_ages (); + for (int n = 0; n < entry_count; n++) { + int x = *((int *)cursor)++; + int y = *((int *)cursor)++; + int age = *((int *)cursor)++; + wrap_tile_coords (&p_bic_data->Map, &x, &y); + int tile_index = tile_coords_to_index (&p_bic_data->Map, x, y); + if ((is->tile_destruct_animation_ages != NULL) && + (tile_index >= 0) && + (tile_index < p_bic_data->Map.TileCount)) { + is->tile_destruct_animation_ages[tile_index] = clamp (0, 255, age); + spawn_selected_tile_animation_for_tile (x, y, true); + } + } + success = true; + } + } + if (! success) { + error_chunk_name = "tile_destruct_animation_ages"; + break; + } + + // ToC-3 + } else if (match_save_chunk_name (&cursor, "great_wall_auto_build_state")) { // Read the single great wall autobuild state variable from the save. Interpret a state of 2 to mean no more great // walls can be built (originally 2 was GWABS_DONE in a now-deleted enum). Otherwise, this variable is no longer used. int state = *((int *)cursor)++; @@ -31169,6 +31935,9 @@ patch_Unit_work_simple_job (Unit * this, int edx, int job_id) Unit_work_simple_job (this, __, job_id); + if (is_worker (this)) + clear_tile_destruct_animation (this->Body.X, this->Body.Y); + if (is->lmify_tile_after_working_simple_job != NULL) is->lmify_tile_after_working_simple_job->vtable->m31_set_landmark (is->lmify_tile_after_working_simple_job, __, true); } @@ -32992,6 +33761,21 @@ init_district_images () char art_dir[200]; char temp_path[2*MAX_PATH]; + int base_season = CS_SUMMER; + if (is->current_config.seasonal_cycle_mode != SCM_OFF) { + int enabled_mask = normalize_enabled_season_mask (is->current_config.enabled_seasons_mask); + base_season = get_first_enabled_season (enabled_mask); + if (is->current_config.seasonal_cycle_mode == SCM_USER_SEASON) { + int user_season = get_current_local_season (); + if (enabled_mask & (1 << user_season)) + base_season = user_season; + } else if (! is->seasonal_cycle_unstarted) { + int current_season = clamp (CS_SUMMER, CS_SPRING, is->current_seasonal_cycle); + if (enabled_mask & (1 << current_season)) + base_season = current_season; + } + } + char const * base_season_name = cycle_season_names[base_season]; is->dc_img_state = IS_INIT_FAILED; @@ -33016,7 +33800,7 @@ init_district_images () continue; // Read PCX file - snprintf (art_dir, sizeof art_dir, "Districts/1200/%s", cfg->img_paths[variant_i]); + snprintf (art_dir, sizeof art_dir, "Districts/%s/1200/%s", base_season_name, cfg->img_paths[variant_i]); get_mod_art_path (art_dir, temp_path, sizeof temp_path); PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); @@ -33057,7 +33841,8 @@ init_district_images () } } // Load abandoned district images (land + maritime) - get_mod_art_path ("Districts/1200/Abandoned.pcx", temp_path, sizeof temp_path); + snprintf (art_dir, sizeof art_dir, "Districts/%s/1200/Abandoned.pcx", base_season_name); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); if (pcx.JGL.Image == NULL) { @@ -33104,7 +33889,7 @@ init_district_images () if (pcx_loaded) wpcx.vtable->clear_JGL (&wpcx); - snprintf (art_dir, sizeof art_dir, "Districts/1200/%s", img_path); + snprintf (art_dir, sizeof art_dir, "Districts/%s/1200/%s", base_season_name, img_path); get_mod_art_path (art_dir, temp_path, sizeof temp_path); PCX_Image_read_file (&wpcx, __, temp_path, NULL, 0, 0x100, 2); @@ -33169,7 +33954,7 @@ init_district_images () if (pcx_loaded) nwpcx.vtable->clear_JGL (&nwpcx); - snprintf (art_dir, sizeof art_dir, "Districts/1200/%s", img_path); + snprintf (art_dir, sizeof art_dir, "Districts/%s/1200/%s", base_season_name, img_path); get_mod_art_path (art_dir, temp_path, sizeof temp_path); PCX_Image_read_file (&nwpcx, __, temp_path, NULL, 0, 0x100, 2); @@ -33200,6 +33985,14 @@ init_district_images () } is->dc_img_state = IS_OK; + + if (((is->current_config.day_night_cycle_mode != DNCM_OFF) || (is->current_config.seasonal_cycle_mode != SCM_OFF)) && + (is->day_night_cycle_img_state == IS_OK)) { + if (is->day_night_cycle_img_proxies_indexed) + deindex_day_night_image_proxies (); + build_sprite_proxies (&p_bic_data->Map.Renderer); + } + pcx.vtable->destruct (&pcx, __, 0); } @@ -33978,6 +34771,23 @@ void draw_district_generated_resource_on_tile (Map_Renderer * this, Tile * tile, struct district_instance * inst, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int visible_to_civ_id) { + int draw_tile_x = tile_x; + int draw_tile_y = tile_y; + if (is->tile_info_open) { + draw_tile_x = is->viewing_tile_info_x; + draw_tile_y = is->viewing_tile_info_y; + } + + int anim_civ_id = visible_to_civ_id; + if (((anim_civ_id < 0) || (anim_civ_id >= 32)) && (p_main_screen_form != NULL)) + anim_civ_id = p_main_screen_form->Player_CivID; + + bool tile_visible_for_animation = false; + if (is->current_config.enable_custom_animations && + ((*p_debug_mode_bits & 0xC) == 0) && + (anim_civ_id >= 0) && (anim_civ_id < 32)) + tile_visible_for_animation = patch_Leader_is_tile_visible (&leaders[anim_civ_id], __, draw_tile_x, draw_tile_y); + int base_resource = Tile_get_resource_visible_to (tile, __, visible_to_civ_id); int district_resource = -1; @@ -34024,10 +34834,43 @@ draw_district_generated_resource_on_tile (Map_Renderer * this, Tile * tile, stru return; } + int district_resource_effect_id = -1; + bool suppress_district_resource_static = + tile_visible_for_animation && + tile_has_matching_resource_animation_for_draw_with_resource (tile, draw_tile_x, draw_tile_y, + district_resource, &district_resource_effect_id); + if (base_resource >= 0) { Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, left_x, pixel_y); } + if (suppress_district_resource_static) { + if ((district_resource_effect_id >= 0) && (tile->Body.active_tile_effect == NULL)) { + struct tile_animation_config * cfg = get_tile_animation_for_effect (district_resource_effect_id); + bool restore_cfg = false; + int saved_x_offset = 0; + bool saved_has_x_offset = false; + + // Match static split-resource placement: generated resource is shifted to the right + // when a native resource is also drawn on the same tile. + if ((cfg != NULL) && (base_resource >= 0)) { + restore_cfg = true; + saved_x_offset = cfg->x_offset; + saved_has_x_offset = cfg->has_x_offset; + cfg->x_offset = saved_x_offset + (offset >> 1); + cfg->has_x_offset = true; + } + + patch_Tile_spawn_animated_effect (tile, __, district_resource_effect_id, draw_tile_x, draw_tile_y, true, DIR_SW); + + if (restore_cfg) { + cfg->x_offset = saved_x_offset; + cfg->has_x_offset = saved_has_x_offset; + } + } + return; + } + int tile_height = tile_width >> 1; int sprite_width = sprite->Width; int sprite_height = sprite->Height; @@ -34114,11 +34957,31 @@ draw_district_on_tile (Map_Renderer * this, Tile * tile, struct district_instanc if (! completed) return; - struct district_config const * cfg = &is->district_configs[district_id]; - struct district_infos * district_info = &is->district_infos[district_id]; - int territory_owner_id = tile->Territory_OwnerID; - int variant = 0; - int era = 0; + struct district_config const * cfg = &is->district_configs[district_id]; + struct district_infos * district_info = &is->district_infos[district_id]; + + if (is->current_config.enable_custom_animations && (tile->Body.active_tile_effect == NULL)) { + refresh_tile_animation_selection_for_tile (tile_x, tile_y); + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + if ((tile_index >= 0) && + is->tile_animation_selected_valid && + (is->tile_animation_selected_next_index != NULL) && + (tile_index < is->tile_animation_selected_tile_count)) { + int animation_index = is->tile_animation_selected_next_index[tile_index]; + if ((animation_index >= 0) && (animation_index < is->tile_animation_count)) { + struct tile_animation_config * anim_cfg = &is->tile_animation_configs[animation_index]; + if ((anim_cfg != NULL) && + anim_cfg->in_use && + (anim_cfg->type == TAT_DISTRICT) && + ((anim_cfg->district_id < 0) || (anim_cfg->district_id == district_id))) + patch_Tile_spawn_animated_effect (tile, __, anim_cfg->effect_id, tile_x, tile_y, true, DIR_SW); + } + } + } + + int territory_owner_id = tile->Territory_OwnerID; + int variant = 0; + int era = 0; int culture = 0; int buildings = 0; int draw_pixel_x = pixel_x; @@ -34300,6 +35163,7 @@ draw_district_on_tile (Map_Renderer * this, Tile * tile, struct district_instanc void __fastcall patch_Map_Renderer_m12_Draw_Tile_Buildings(Map_Renderer * this, int edx, int visible_to_civ_id, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) { + //*p_debug_mode_bits |= 0xC; if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) { Map_Renderer_m12_Draw_Tile_Buildings(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); return; @@ -34329,32 +35193,58 @@ patch_Map_Renderer_m12_Draw_Tile_Buildings(Map_Renderer * this, int edx, int vis void __fastcall patch_Map_Renderer_m09_Draw_Tile_Resources (Map_Renderer * this, int edx, int visible_to_civ_id, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) { - if (! is->current_config.enable_districts) { - Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); - return; - } - Tile * tile = is->current_render_tile; + int draw_tile_x = tile_x; + int draw_tile_y = tile_y; if (is->tile_info_open) tile = tile_at (is->viewing_tile_info_x, is->viewing_tile_info_y); - if ((tile == NULL) || (tile == p_null_tile)) { - Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); - return; + if (is->tile_info_open) { + draw_tile_x = is->viewing_tile_info_x; + draw_tile_y = is->viewing_tile_info_y; + } + + bool suppress_static_resource = false; + int resource_animation_effect_id = -1; + int anim_civ_id = visible_to_civ_id; + if (((anim_civ_id < 0) || (anim_civ_id >= 32)) && (p_main_screen_form != NULL)) + anim_civ_id = p_main_screen_form->Player_CivID; + bool tile_visible_for_animation = false; + if (is->current_config.enable_custom_animations && + ((*p_debug_mode_bits & 0xC) == 0) && + (tile != NULL) && (tile != p_null_tile) && + (anim_civ_id >= 0) && (anim_civ_id < 32)) + tile_visible_for_animation = patch_Leader_is_tile_visible (&leaders[anim_civ_id], __, draw_tile_x, draw_tile_y); + + if (tile_visible_for_animation) { + int visible_resource_id = Tile_get_resource_visible_to (tile, __, anim_civ_id); + suppress_static_resource = tile_has_matching_resource_animation_for_draw_with_resource (tile, draw_tile_x, draw_tile_y, + visible_resource_id, &resource_animation_effect_id); + if (suppress_static_resource && (resource_animation_effect_id >= 0) && (tile->Body.active_tile_effect == NULL)) + patch_Tile_spawn_animated_effect (tile, __, resource_animation_effect_id, draw_tile_x, draw_tile_y, true, DIR_SW); } - struct district_instance * inst = is->current_render_tile_district; - if (is->tile_info_open) - inst = get_district_instance (tile); - if (inst == NULL) { - Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + if ((tile == NULL) || (tile == p_null_tile)) { + if (! suppress_static_resource) + Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); return; } - // Resources that should be drawn below district are already drawn, skip in that case - if (is->district_configs[inst->district_id].draw_over_resources) - return; + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + struct district_instance * inst = is->current_render_tile_district; + if (is->tile_info_open) + inst = get_district_instance (tile); + if (inst == NULL) { + if (! suppress_static_resource) + Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } + + // Resources that should be drawn below district are already drawn, skip in that case + if (is->district_configs[inst->district_id].draw_over_resources) + return; - draw_district_generated_resource_on_tile (this, tile, inst, tile_x, tile_y, map_renderer, pixel_x, pixel_y, visible_to_civ_id); + draw_district_generated_resource_on_tile (this, tile, inst, tile_x, tile_y, map_renderer, pixel_x, pixel_y, visible_to_civ_id); + } } void __fastcall @@ -36911,6 +37801,2058 @@ patch_Tile_check_water_for_canal_move_to_adjacent_tile_dest (Tile * this) return this->vtable->m35_Check_Is_Water (this); } +int +pack_tile_animation_pcx_lookup_value (int pcx_file_id, int pcx_index) +{ + int file_bits = (pcx_file_id < 0) ? 0 : (pcx_file_id + 1); + return (file_bits << 12) | (pcx_index & 0xFFF); +} + +bool +unpack_tile_animation_pcx_lookup_value (int packed, int * out_pcx_file_id, int * out_pcx_index) +{ + int file_bits = (packed >> 12) & 0xFFFFF; + if (file_bits <= 0) + return false; + if (out_pcx_file_id != NULL) + *out_pcx_file_id = file_bits - 1; + if (out_pcx_index != NULL) + *out_pcx_index = packed & 0xFFF; + return true; +} + +void +insert_tile_animation_pcx_sprite_mapping (Sprite * sprite, int pcx_file_id, int pcx_index) +{ + if ((sprite == NULL) || (sprite->vtable == NULL)) + return; + itable_insert (&is->tile_animation_pcx_sprite_lookup, (int)sprite, pack_tile_animation_pcx_lookup_value (pcx_file_id, pcx_index)); +} + +void +insert_tile_animation_pcx_sprite_range (Sprite * sprites, int count, int pcx_file_id) +{ + if ((sprites == NULL) || (count <= 0)) + return; + for (int i = 0; i < count; i++) + insert_tile_animation_pcx_sprite_mapping (&sprites[i], pcx_file_id, i); +} + +void +clear_tile_animation_pcx_sprite_lookup () +{ + table_deinit (&is->tile_animation_pcx_sprite_lookup); + is->tile_animation_pcx_sprite_lookup = (struct table) {0}; +} + +void +clear_tile_animation_pcx_rule_lookup () +{ + // Rule lookup maps packed (pcx_file_id, pcx_index) -> rule-mask row index. + table_deinit (&is->tile_animation_pcx_rule_key_to_index); + is->tile_animation_pcx_rule_key_to_index = (struct table) {0}; + is->tile_animation_pcx_rule_key_count = 0; + memset (is->tile_animation_pcx_rule_masks, 0, sizeof is->tile_animation_pcx_rule_masks); +} + +void +rebuild_tile_animation_pcx_sprite_lookup () +{ + // Build once-per-map_renderer pointers: Sprite* -> packed (pcx_file_id, pcx_index). + // This lets draw-hook registration avoid any name/index inference work. + clear_tile_animation_pcx_sprite_lookup (); + + Map_Renderer * mr = &p_bic_data->Map.Renderer; + insert_tile_animation_pcx_sprite_mapping (&mr->Terrain_Buldings_Mines, TAPF_TERRAINBUILDINGS, 0); + insert_tile_animation_pcx_sprite_range (mr->Waterfalls_Images, 4, TAPF_WATERFALLS); + insert_tile_animation_pcx_sprite_range (mr->Flood_Plains_Images, 16, TAPF_FLOODPLAINS); + insert_tile_animation_pcx_sprite_range (mr->Delta_Rivers_Images, 16, TAPF_DELTARIVERS); + insert_tile_animation_pcx_sprite_range (mr->Mountain_Rivers_Images, 16, TAPF_MTNRIVERS); + insert_tile_animation_pcx_sprite_range (mr->Irrigation_Desert_Images, 16, TAPF_IRRIGATION_DESETT); + insert_tile_animation_pcx_sprite_range (mr->Irrigation_Plains_Images, 16, TAPF_IRRIGATION_PLAINS); + insert_tile_animation_pcx_sprite_range (mr->Irrigation_Images, 16, TAPF_IRRIGATION); + insert_tile_animation_pcx_sprite_range (mr->Irrigation_Tundra_Images, 16, TAPF_IRRIGATION_TUNDRA); + insert_tile_animation_pcx_sprite_range (mr->Volcanos_Images, 16, TAPF_VOLCANOS); + insert_tile_animation_pcx_sprite_range (mr->Volcanos_Forests_Images, 16, TAPF_VOLCANOS_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Volcanos_Jungles_Images, 16, TAPF_VOLCANOS_JUNGLES); + insert_tile_animation_pcx_sprite_range (mr->Volcanos_Snow_Images, 16, TAPF_VOLCANOS_SNOW); + insert_tile_animation_pcx_sprite_range (mr->Grassland_Forests_Large, 8, TAPF_GRASSLAND_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Grassland_Forests_Small, 10, TAPF_GRASSLAND_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Grassland_Forests_Pines, 12, TAPF_GRASSLAND_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Plains_Forests_Large, 8, TAPF_PLAINS_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Plains_Forests_Small, 10, TAPF_PLAINS_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Plains_Forests_Pines, 12, TAPF_PLAINS_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Tundra_Forests_Large, 8, TAPF_TUNDRA_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Tundra_Forests_Small, 10, TAPF_TUNDRA_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Tundra_Forests_Pines, 12, TAPF_TUNDRA_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->LM_Forests_Large_Images, 8, TAPF_LMFORESTS); + insert_tile_animation_pcx_sprite_range (mr->LM_Forests_Small_Images, 10, TAPF_LMFORESTS); + insert_tile_animation_pcx_sprite_range (mr->LM_Forests_Pines_Images, 12, TAPF_LMFORESTS); + insert_tile_animation_pcx_sprite_range (mr->Mountains_Images, 16, TAPF_MOUNTAINS); + insert_tile_animation_pcx_sprite_range (mr->Mountains_Forests_Images, 16, TAPF_MOUNTAIN_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Mountains_Jungles_Images, 16, TAPF_MOUNTAIN_JUNGLES); + insert_tile_animation_pcx_sprite_range (mr->Mountains_Snow_Images, 16, TAPF_MOUNTAINS_SNOW); + insert_tile_animation_pcx_sprite_range (mr->Hills_Images, 16, TAPF_XHILLS); + insert_tile_animation_pcx_sprite_range (mr->Hills_Forests_Images, 16, TAPF_HILL_FORESTS); + insert_tile_animation_pcx_sprite_range (mr->Hills_Jungle_Images, 16, TAPF_HILL_JUNGLE); + insert_tile_animation_pcx_sprite_range (mr->LM_Hills_Images, 16, TAPF_LMHILLS); + insert_tile_animation_pcx_sprite_range (mr->Roads_Images, 256, TAPF_ROADS); + insert_tile_animation_pcx_sprite_range (mr->Railroads_Images, 272, TAPF_RAILROADS); +} + +bool +read_tile_animation_pcx_file (struct string_slice const * s, int * out_id) +{ + struct string_slice trimmed = trim_string_slice (s, 1); + int id = TILE_ANIM_PCX_FILE_UNKNOWN; + if (slice_matches_str (&trimmed, "deltaRivers.pcx")) id = TAPF_DELTARIVERS; + else if (slice_matches_str (&trimmed, "floodplains.pcx")) id = TAPF_FLOODPLAINS; + else if (slice_matches_str (&trimmed, "LMHills.pcx")) id = TAPF_LMHILLS; + else if (slice_matches_str (&trimmed, "Mountains.pcx")) id = TAPF_MOUNTAINS; + else if (slice_matches_str (&trimmed, "Mountains-snow.pcx")) id = TAPF_MOUNTAINS_SNOW; + else if (slice_matches_str (&trimmed, "mtnRivers.pcx")) id = TAPF_MTNRIVERS; + else if (slice_matches_str (&trimmed, "Volcanos.pcx")) id = TAPF_VOLCANOS; + else if (slice_matches_str (&trimmed, "Volcanos-snow.pcx")) id = TAPF_VOLCANOS_SNOW; + else if (slice_matches_str (&trimmed, "waterfalls.pcx")) id = TAPF_WATERFALLS; + else if (slice_matches_str (&trimmed, "xhills.pcx")) id = TAPF_XHILLS; + + if (id == TILE_ANIM_PCX_FILE_UNKNOWN) + return false; + if (out_id != NULL) + *out_id = id; + return true; +} + +void +refresh_tile_animation_pcx_rule_mask () +{ + // Precompute two things: + // 1) which animation bits are PCX-driven at all (tile_animation_pcx_word_mask), + // 2) key-specific bitmasks for fast per-draw assignment. + int words_per_tile = (MAX_TILE_ANIMATION_CONFIGS + 31) / 32; + for (int w = 0; w < words_per_tile; w++) + is->tile_animation_pcx_word_mask[w] = 0; + for (int w = 0; w < words_per_tile; w++) + is->tile_animation_pcx_active_word_mask[w] = 0; + is->tile_animation_has_pcx_rules = false; + clear_tile_animation_pcx_rule_lookup (); + + for (int i = 0; i < is->tile_animation_count; i++) { + struct tile_animation_config const * cfg = &is->tile_animation_configs[i]; + if (! cfg->in_use || (cfg->type != TAT_PCX)) + continue; + is->tile_animation_pcx_word_mask[i / 32] |= 1u << (i % 32); + is->tile_animation_has_pcx_rules = true; + + int packed = pack_tile_animation_pcx_lookup_value (cfg->pcx_file_id, cfg->pcx_index); + int key_index = -1; + if (! itable_look_up (&is->tile_animation_pcx_rule_key_to_index, packed, &key_index)) { + if (is->tile_animation_pcx_rule_key_count >= MAX_TILE_ANIMATION_CONFIGS) + continue; + key_index = is->tile_animation_pcx_rule_key_count++; + itable_insert (&is->tile_animation_pcx_rule_key_to_index, packed, key_index); + } + // One row per unique (pcx_file, pcx_index), bits mark matching animation configs. + is->tile_animation_pcx_rule_masks[key_index][i / 32] |= 1u << (i % 32); + } + + refresh_tile_animation_pcx_active_mask (); +} + +void +refresh_tile_animation_pcx_active_mask () +{ + // Time/season gating is recomputed once per Draw_Tiles pass, then ANDed at draw time. + int words_per_tile = (MAX_TILE_ANIMATION_CONFIGS + 31) / 32; + for (int w = 0; w < words_per_tile; w++) + is->tile_animation_pcx_active_word_mask[w] = 0; + if (! is->tile_animation_has_pcx_rules) + return; + + for (int i = 0; i < is->tile_animation_count; i++) { + struct tile_animation_config const * cfg = &is->tile_animation_configs[i]; + if (! cfg->in_use || (cfg->type != TAT_PCX)) + continue; + if (! tile_animation_matches_time_filters (cfg)) + continue; + is->tile_animation_pcx_active_word_mask[i / 32] |= 1u << (i % 32); + } +} + +void +clear_tile_animation_pcx_matches_in_cache () +{ + // PCX matches are dynamic because they depend on what sprites were actually drawn this frame. + // Keep static terrain/resource cache bits, clear only the PCX-controlled bits. + if (! is->tile_animation_has_pcx_rules) + return; + if (! is->tile_animation_selected_valid || (is->tile_animation_selected_mask_matrix == NULL)) + return; + if ((is->tile_animation_selected_next_index == NULL) || (is->tile_animation_selected_tile_indices == NULL)) + return; + int tile_count = p_bic_data->Map.TileCount; + if (tile_count <= 0) + return; + if (is->tile_animation_selected_tile_count <= 0) + return; + if (tile_count > is->tile_animation_selected_tile_count) + tile_count = is->tile_animation_selected_tile_count; + + int words_per_tile = (MAX_TILE_ANIMATION_CONFIGS + 31) / 32; + is->tile_animation_selected_match_count = 0; + for (int tile_index = 0; tile_index < tile_count; tile_index++) { + unsigned int * tile_mask = is->tile_animation_selected_mask_matrix + tile_index * words_per_tile; + for (int w = 0; w < words_per_tile; w++) { + unsigned int clear_mask = is->tile_animation_pcx_word_mask[w]; + if (clear_mask != 0) + tile_mask[w] &= ~clear_mask; + } + + int winner = pick_tile_animation_winner_for_tile (tile_mask); + if ((winner >= 0) && (winner < is->tile_animation_count)) { + is->tile_animation_selected_next_index[tile_index] = winner; + is->tile_animation_selected_tile_indices[is->tile_animation_selected_match_count++] = tile_index; + } else + is->tile_animation_selected_next_index[tile_index] = 0xFF; + } +} + +void +register_tile_animation_pcx_draw_for_current_tile (Sprite * sprite) +{ + // Called from patch_Sprite_draw_on_map while current_render_tile_* points at the tile being drawn. + // We mark candidate animations onto that tile's mask; spawning still happens later in scheduler_tick. + if (! is->tile_animation_has_pcx_rules) + return; + if (! is->tile_animation_selected_valid || + (is->tile_animation_selected_mask_matrix == NULL) || + (is->current_render_tile == NULL) || + (is->current_render_tile == p_null_tile)) + return; + if ((is->current_render_tile_x < 0) || (is->current_render_tile_y < 0)) + return; + + int packed = 0; + if (! itable_look_up (&is->tile_animation_pcx_sprite_lookup, (int)sprite, &packed)) + return; + + int pcx_file_id = TILE_ANIM_PCX_FILE_UNKNOWN; + int pcx_index = -1; + if (! unpack_tile_animation_pcx_lookup_value (packed, &pcx_file_id, &pcx_index)) + return; + + int tile_index = tile_coords_to_index (&p_bic_data->Map, is->current_render_tile_x, is->current_render_tile_y); + if ((tile_index < 0) || (tile_index >= is->tile_animation_selected_tile_count)) + return; + + int words_per_tile = (MAX_TILE_ANIMATION_CONFIGS + 31) / 32; + unsigned int * tile_mask = is->tile_animation_selected_mask_matrix + tile_index * words_per_tile; + byte prev_winner = is->tile_animation_selected_next_index[tile_index]; + int key_index = -1; + // Exact file+index match. + if (itable_look_up (&is->tile_animation_pcx_rule_key_to_index, packed, &key_index) && + (key_index >= 0) && + (key_index < is->tile_animation_pcx_rule_key_count)) { + for (int w = 0; w < words_per_tile; w++) + tile_mask[w] |= is->tile_animation_pcx_rule_masks[key_index][w] & is->tile_animation_pcx_active_word_mask[w]; + } + + if (pcx_index >= 0) { + // Also allow wildcard rules keyed as (same file, index = -1). + int wildcard_packed = pack_tile_animation_pcx_lookup_value (pcx_file_id, -1); + if ((wildcard_packed != packed) && + itable_look_up (&is->tile_animation_pcx_rule_key_to_index, wildcard_packed, &key_index) && + (key_index >= 0) && + (key_index < is->tile_animation_pcx_rule_key_count)) { + for (int w = 0; w < words_per_tile; w++) + tile_mask[w] |= is->tile_animation_pcx_rule_masks[key_index][w] & is->tile_animation_pcx_active_word_mask[w]; + } + } + + int winner = pick_tile_animation_winner_for_tile (tile_mask); + if ((winner >= 0) && (winner < is->tile_animation_count)) { + is->tile_animation_selected_next_index[tile_index] = winner; + if ((prev_winner == 0xFF) && (is->tile_animation_selected_tile_indices != NULL) && + (is->tile_animation_selected_match_count < is->tile_animation_selected_tile_count)) + is->tile_animation_selected_tile_indices[is->tile_animation_selected_match_count++] = tile_index; + } else + is->tile_animation_selected_next_index[tile_index] = 0xFF; +} + +bool +read_tile_animation_terrain_types (struct string_slice const * value, + unsigned int * out_mask, + bool * out_include_land) +{ + char * text = extract_slice (value); + if (text == NULL) + return false; + + unsigned int mask = 0; + bool include_land = false; + bool saw_token = false; + char * cursor = text; + while (*cursor != '\0') { + while ((*cursor == ' ') || (*cursor == '\t') || (*cursor == ',')) + cursor++; + if (*cursor == '\0') + break; + + char * token_start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; + struct string_slice token = { .str = token_start, .len = cursor - token_start }; + token = trim_string_slice (&token, 1); + if (token.len > 0) { + saw_token = true; + if (slice_matches_str (&token, "land")) + include_land = true; + else { + enum SquareTypes terrain = SQ_INVALID; + if (! read_tile_terrain_type_value (&token, &terrain)) { + free (text); + return false; + } + if (terrain == SQ_INVALID) { + mask |= all_square_types_mask (); + include_land = true; + } else { + unsigned int bit = square_type_mask_bit (terrain); + if (bit == 0) { + free (text); + return false; + } + mask |= bit; + } + } + } + + if (*cursor == ',') + cursor++; + } + + free (text); + if (! saw_token) + return false; + if (out_mask != NULL) + *out_mask = mask; + if (out_include_land != NULL) + *out_include_land = include_land; + return true; +} + +bool +tile_matches_terrain_types (Tile * tile, unsigned int terrain_types_mask, bool include_land) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + if (include_land && ! tile->vtable->m35_Check_Is_Water (tile)) + return true; + return tile_matches_square_type_mask (tile, terrain_types_mask); +} + +bool +get_tile_animation_coastal_wave_direction (int tile_x, int tile_y, enum direction * out_dir) +{ + bool nw_is_land = ! tile_is_water (tile_x - 1, tile_y - 1); + bool ne_is_land = ! tile_is_water (tile_x + 1, tile_y - 1); + bool se_is_land = ! tile_is_water (tile_x + 1, tile_y + 1); + bool sw_is_land = ! tile_is_water (tile_x - 1, tile_y + 1); + + if (nw_is_land && ! se_is_land && ! ne_is_land && ! sw_is_land) { + *out_dir = DIR_NW; + return true; + } + if (ne_is_land && ! sw_is_land && ! se_is_land && !nw_is_land) { + *out_dir = DIR_NE; + return true; + } + if (sw_is_land && ! ne_is_land && ! se_is_land && ! nw_is_land) { + *out_dir = DIR_SW; + return true; + } + if (se_is_land && ! nw_is_land && ! ne_is_land && ! sw_is_land) { + *out_dir = DIR_SE; + return true; + } + return false; +} + +bool +tile_animation_adjacent_requirement_matches (struct tile_animation_adjacent_requirement const * req, + int tile_x, + int tile_y) +{ + if (req == NULL) + return false; + + int diffs[8][2] = { + { 1, -1}, { 2, 0}, { 1, 1}, { 0, 2}, + {-1, 1}, {-2, 0}, {-1, -1}, { 0, -2} + }; + + int begin = 0, end = 8; + if (req->has_direction) { + begin = req->direction - DIR_NE; + end = begin + 1; + } + + for (int i = begin; i < end; i++) { + int nx = tile_x + diffs[i][0]; + int ny = tile_y + diffs[i][1]; + wrap_tile_coords (&p_bic_data->Map, &nx, &ny); + Tile * n = tile_at (nx, ny); + if ((n == NULL) || (n == p_null_tile)) + continue; + + bool matches = req->is_land ? ! n->vtable->m35_Check_Is_Water (n) : tile_matches_square_type (n, req->square_type); + if (matches) + return true; + } + + return false; +} + +int +get_tile_animation_hour_for_match () +{ + if (is->current_config.day_night_cycle_mode != DNCM_OFF) + return clamp (0, 23, is->current_day_night_cycle); + return 12; +} + +int +get_tile_animation_season_for_match () +{ + if (is->current_config.seasonal_cycle_mode != SCM_OFF) + return clamp (CS_SUMMER, CS_SPRING, is->current_seasonal_cycle); + return CS_SUMMER; +} + +bool +tile_animation_matches_time_filters (struct tile_animation_config const * cfg) +{ + if (cfg == NULL) + return false; + + int hour = get_tile_animation_hour_for_match (); + int season = get_tile_animation_season_for_match (); + if ((cfg->day_night_hour_mask != 0) && ((cfg->day_night_hour_mask & (1u << hour)) == 0)) + return false; + if ((cfg->season_mask != 0) && ((cfg->season_mask & (1u << season)) == 0)) + return false; + return true; +} + +bool +district_animation_instance_is_complete (Tile * tile, struct district_instance * inst) +{ + if ((tile == NULL) || (tile == p_null_tile) || (inst == NULL)) + return false; + if (inst->state == DS_COMPLETED) + return true; + return tile->vtable->m18_Check_Mines (tile, __, 0) != 0; +} + +bool +get_district_animation_culture_and_era (Tile * tile, struct district_instance * inst, int * out_culture, int * out_era) +{ + if ((tile == NULL) || (tile == p_null_tile) || (inst == NULL)) + return false; + + int culture = 0; + int era = 0; + int civ_id = tile->vtable->m38_Get_Territory_OwnerID (tile); + if ((civ_id < 0) && (inst->built_by_civ_id >= 0)) + civ_id = inst->built_by_civ_id; + + if ((civ_id >= 0) && (civ_id < 32)) { + Leader * leader = &leaders[civ_id]; + if ((leader->RaceID >= 0) && (leader->RaceID < p_bic_data->RacesCount)) + culture = p_bic_data->Races[leader->RaceID].CultureGroupID; + era = leader->Era; + } + + if (out_culture != NULL) + *out_culture = culture; + if (out_era != NULL) + *out_era = era; + return true; +} + +bool +tile_animation_rule_matches_tile_base (struct tile_animation_config const * cfg, Tile * tile, int tile_x, int tile_y) +{ + if ((cfg == NULL) || (! cfg->in_use) || (tile == NULL) || (tile == p_null_tile)) + return false; + + if (Tile_has_city (tile)) + return false; + + if (cfg->type == TAT_RESOURCE) { + int resource_id = tile->vtable->m39_Get_Resource_Type (tile); + if (resource_id != cfg->resource_id) + return false; + } else if (cfg->type == TAT_NATURAL_WONDER) { + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || (inst->district_id != NATURAL_WONDER_DISTRICT_ID)) + return false; + if ((cfg->natural_wonder_id >= 0) && + (inst->natural_wonder_info.natural_wonder_id != cfg->natural_wonder_id)) + return false; + } else if (cfg->type == TAT_DISTRICT) { + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || (inst->district_id == NATURAL_WONDER_DISTRICT_ID)) + return false; + if ((cfg->district_id >= 0) && (inst->district_id != cfg->district_id)) + return false; + if (! district_animation_instance_is_complete (tile, inst)) + return false; + if ((cfg->culture_group_mask != 0) || (cfg->era_mask != 0)) { + int culture = 0, era = 0; + if (! get_district_animation_culture_and_era (tile, inst, &culture, &era)) + return false; + if ((cfg->culture_group_mask != 0) && + ((culture < 0) || (culture >= 32) || ((cfg->culture_group_mask & (1u << culture)) == 0))) + return false; + if ((cfg->era_mask != 0) && + ((era < 0) || (era >= 32) || ((cfg->era_mask & (1u << era)) == 0))) + return false; + } + } else if (cfg->type == TAT_DESTRUCT_INITIAL) { + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + if (! tile_has_destruct_animation_age (tile_index, 1)) + return false; + } else if (cfg->type == TAT_DESTRUCT_AFTER) { + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + if (! tile_has_any_destruct_animation_age (tile_index) || + tile_has_destruct_animation_age (tile_index, 1)) + return false; + } else if (cfg->type == TAT_TERRAIN) { + if (! tile_matches_terrain_types (tile, cfg->terrain_types_mask, cfg->terrain_types_include_land)) + return false; + } else if (cfg->type == TAT_COASTAL_WAVE) { + if (! tile_matches_square_type (tile, SQ_Coast)) + return false; + } else { + // PCX-based animations are discovered from actual draw calls in patch_Sprite_draw_on_map. + return false; + } + + if (cfg->adjacent_to_count > 0) { + bool matched = false; + for (int i = 0; i < cfg->adjacent_to_count; i++) + if (tile_animation_adjacent_requirement_matches (&cfg->adjacent_to[i], tile_x, tile_y)) { + matched = true; + break; + } + if (! matched) + return false; + } + + if (cfg->type == TAT_COASTAL_WAVE) { + enum direction dir = DIR_ZERO; + if (! get_tile_animation_coastal_wave_direction (tile_x, tile_y, &dir)) + return false; + } + + return true; +} + +void +free_tile_animation_selected_matrix () +{ + if (is->tile_animation_selected_mask_matrix != NULL) + free (is->tile_animation_selected_mask_matrix); + if (is->tile_animation_selected_next_index != NULL) + free (is->tile_animation_selected_next_index); + if (is->tile_animation_selected_tile_indices != NULL) + free (is->tile_animation_selected_tile_indices); + is->tile_animation_selected_mask_matrix = NULL; + is->tile_animation_selected_next_index = NULL; + is->tile_animation_selected_tile_indices = NULL; + is->tile_animation_selected_match_count = 0; + is->tile_animation_selected_tile_count = 0; + is->tile_animation_selected_animation_count = 0; + is->tile_animation_selected_valid = false; +} + +bool +is_tile_destruct_animation_type (enum tile_animation_type type) +{ + return (type == TAT_DESTRUCT_INITIAL) || (type == TAT_DESTRUCT_AFTER); +} + +bool +tile_has_destruct_animation_age (int tile_index, int age) +{ + return (is->tile_destruct_animation_ages != NULL) && + (tile_index >= 0) && + (tile_index < p_bic_data->Map.TileCount) && + (is->tile_destruct_animation_ages[tile_index] == age); +} + +bool +tile_has_any_destruct_animation_age (int tile_index) +{ + return (is->tile_destruct_animation_ages != NULL) && + (tile_index >= 0) && + (tile_index < p_bic_data->Map.TileCount) && + (is->tile_destruct_animation_ages[tile_index] > 0); +} + +void +ensure_tile_destruct_animation_ages () +{ + int tile_count = p_bic_data->Map.TileCount; + if (tile_count <= 0) + return; + if (is->tile_destruct_animation_ages != NULL) + return; + is->tile_destruct_animation_ages = calloc (tile_count, sizeof *is->tile_destruct_animation_ages); +} + +void +hide_active_custom_tile_animation (Tile * tile, int winner_index) +{ + if ((tile == NULL) || (tile == p_null_tile) || (tile->Body.active_tile_effect == NULL)) + return; + + int effect_id = tile->Body.active_tile_effect->V[2]; + struct tile_animation_config * cfg = get_tile_animation_for_effect (effect_id); + if (cfg == NULL) + return; + if ((winner_index >= 0) && + (winner_index < is->tile_animation_count) && + (cfg->effect_id == is->tile_animation_configs[winner_index].effect_id)) + return; + + tile->Body.active_tile_effect->flc_animation.summary.current_anim_type = AT_DEFAULT; + tile->Body.active_tile_effect->flc_animation.summary.queued_anim_type = AT_BLANK; +} + +void +refresh_tile_animation_selection_for_tile (int tile_x, int tile_y) +{ + if (! is->current_config.enable_custom_animations) + return; + if (tile_animation_cache_needs_rebuild ()) { + rebuild_tile_animation_rule_match_cache (); + return; + } + if (! is->tile_animation_selected_valid || + (is->tile_animation_selected_mask_matrix == NULL) || + (is->tile_animation_selected_next_index == NULL) || + (is->tile_animation_selected_tile_indices == NULL)) + return; + if ((tile_x < 0) || (tile_y < 0) || (tile_x >= p_bic_data->Map.Width) || (tile_y >= p_bic_data->Map.Height)) + return; + + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + if ((tile_index < 0) || (tile_index >= is->tile_animation_selected_tile_count)) + return; + + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return; + + int words_per_tile = (MAX_TILE_ANIMATION_CONFIGS + 31) / 32; + unsigned int * tile_mask = is->tile_animation_selected_mask_matrix + tile_index * words_per_tile; + for (int w = 0; w < words_per_tile; w++) + tile_mask[w] = 0; + + for (int i = 0; i < is->tile_animation_count; i++) { + struct tile_animation_config const * cfg = &is->tile_animation_configs[i]; + if (! cfg->in_use) + continue; + if (tile_animation_matches_time_filters (cfg) && + tile_animation_rule_matches_tile_base (cfg, tile, tile_x, tile_y)) + tile_mask[i / 32] |= 1u << (i % 32); + } + + byte prev_winner = is->tile_animation_selected_next_index[tile_index]; + int winner = pick_tile_animation_winner_for_tile (tile_mask); + hide_active_custom_tile_animation (tile, winner); + if ((winner >= 0) && (winner < is->tile_animation_count)) { + is->tile_animation_selected_next_index[tile_index] = winner; + if ((prev_winner == 0xFF) && + (is->tile_animation_selected_match_count < is->tile_animation_selected_tile_count)) + is->tile_animation_selected_tile_indices[is->tile_animation_selected_match_count++] = tile_index; + } else { + is->tile_animation_selected_next_index[tile_index] = 0xFF; + } +} + +void +spawn_selected_tile_animation_for_tile (int tile_x, int tile_y, bool destruct_only) +{ + if (! is->current_config.enable_custom_animations) + return; + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile) || Tile_has_city (tile)) + return; + + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + if ((tile_index < 0) || (tile_index >= p_bic_data->Map.TileCount)) + return; + + refresh_tile_animation_selection_for_tile (tile_x, tile_y); + if ((! is->tile_animation_selected_valid) || + (is->tile_animation_selected_next_index == NULL) || + (tile_index >= is->tile_animation_selected_tile_count)) + return; + + int winner = is->tile_animation_selected_next_index[tile_index]; + if ((winner >= 0) && (winner < is->tile_animation_count)) { + struct tile_animation_config * cfg = &is->tile_animation_configs[winner]; + if ((cfg == NULL) || (! cfg->in_use)) + return; + if (destruct_only && ! is_tile_destruct_animation_type (cfg->type)) + return; + bool can_spawn = tile->Body.active_tile_effect == NULL; + if (! can_spawn) { + struct tile_animation_config * active_cfg = get_tile_animation_for_effect (tile->Body.active_tile_effect->V[2]); + can_spawn = (active_cfg != NULL) && + (is_tile_destruct_animation_type (active_cfg->type) || + (get_tile_animation_type_priority (active_cfg->type) < get_tile_animation_type_priority (cfg->type))); + } + if (can_spawn) + patch_Tile_spawn_animated_effect (tile, __, cfg->effect_id, tile_x, tile_y, true, DIR_SW); + } +} + +void +trigger_tile_destruct_animation (int tile_x, int tile_y, int trigger) +{ + if (! is->current_config.enable_custom_animations) + return; + if ((is->current_config.show_tile_destruct_animation_after & trigger) == 0) + return; + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile) || Tile_has_city (tile)) + return; + + ensure_tile_destruct_animation_ages (); + if (is->tile_destruct_animation_ages == NULL) + return; + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + if ((tile_index < 0) || (tile_index >= p_bic_data->Map.TileCount)) + return; + + is->tile_destruct_animation_ages[tile_index] = 1; + spawn_selected_tile_animation_for_tile (tile_x, tile_y, true); +} + +void +clear_tile_destruct_animation (int tile_x, int tile_y) +{ + if (is->tile_destruct_animation_ages == NULL) + return; + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + if ((tile_index < 0) || (tile_index >= p_bic_data->Map.TileCount)) + return; + if (is->tile_destruct_animation_ages[tile_index] == 0) + return; + is->tile_destruct_animation_ages[tile_index] = 0; + refresh_tile_animation_selection_for_tile (tile_x, tile_y); +} + +void +age_tile_destruct_animations () +{ + if ((! is->current_config.enable_custom_animations) || (is->tile_destruct_animation_ages == NULL)) + return; + Map * map = &p_bic_data->Map; + int tile_count = map->TileCount; + int max_turns = not_below (0, is->current_config.show_tile_destruction_animation_for_turns); + for (int tile_index = 0; tile_index < tile_count; tile_index++) { + byte age = is->tile_destruct_animation_ages[tile_index]; + if (age == 0) + continue; + age++; + is->tile_destruct_animation_ages[tile_index] = age; + int tile_x, tile_y; + tile_index_to_coords (map, tile_index, &tile_x, &tile_y); + if (age > max_turns) + clear_tile_destruct_animation (tile_x, tile_y); + else + spawn_selected_tile_animation_for_tile (tile_x, tile_y, true); + } +} + +void +rebuild_tile_animation_rule_match_cache () +{ + Map * map = &p_bic_data->Map; + int tile_count = map->TileCount; + if ((tile_count <= 0) || (is->tile_animation_count <= 0)) { + free_tile_animation_selected_matrix (); + return; + } + + int words_per_tile = (MAX_TILE_ANIMATION_CONFIGS + 31) / 32; + free_tile_animation_selected_matrix (); + is->tile_animation_selected_mask_matrix = calloc (tile_count * words_per_tile, sizeof *is->tile_animation_selected_mask_matrix); + is->tile_animation_selected_next_index = malloc (tile_count * sizeof *is->tile_animation_selected_next_index); + is->tile_animation_selected_tile_indices = malloc (tile_count * sizeof *is->tile_animation_selected_tile_indices); + if ((is->tile_animation_selected_mask_matrix == NULL) || + (is->tile_animation_selected_next_index == NULL) || + (is->tile_animation_selected_tile_indices == NULL)) { + free_tile_animation_selected_matrix (); + return; + } + memset (is->tile_animation_selected_next_index, 0xFF, tile_count * sizeof *is->tile_animation_selected_next_index); + is->tile_animation_selected_match_count = 0; + + for (int tile_index = 0; tile_index < tile_count; tile_index++) { + int tile_x, tile_y; + tile_index_to_coords (map, tile_index, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + unsigned int * tile_mask = is->tile_animation_selected_mask_matrix + tile_index * words_per_tile; + + for (int i = 0; i < is->tile_animation_count; i++) { + struct tile_animation_config const * cfg = &is->tile_animation_configs[i]; + if (! cfg->in_use) + continue; + + bool matches = tile_animation_matches_time_filters (cfg) + && tile_animation_rule_matches_tile_base (cfg, tile, tile_x, tile_y); + if (! matches) + continue; + + tile_mask[i / 32] |= 1u << (i % 32); + } + + int winner = pick_tile_animation_winner_for_tile (tile_mask); + if ((winner >= 0) && (winner < is->tile_animation_count)) { + is->tile_animation_selected_next_index[tile_index] = winner; + is->tile_animation_selected_tile_indices[is->tile_animation_selected_match_count++] = tile_index; + } + } + + is->tile_animation_selected_tile_count = tile_count; + is->tile_animation_selected_animation_count = is->tile_animation_count; + is->tile_animation_selected_valid = true; +} + +bool +tile_animation_cache_needs_rebuild () +{ + if (! is->tile_animation_selected_valid) + return true; + if (is->tile_animation_selected_mask_matrix == NULL) + return true; + if (is->tile_animation_selected_next_index == NULL) + return true; + if (is->tile_animation_selected_tile_indices == NULL) + return true; + if (is->tile_animation_count <= 0) + return true; + if (is->tile_animation_selected_tile_count != p_bic_data->Map.TileCount) + return true; + if (is->tile_animation_selected_animation_count != is->tile_animation_count) + return true; + return false; +} + +bool +tile_animation_rule_matches_tile (struct tile_animation_config const * cfg, Tile * tile, int tile_x, int tile_y, bool for_draw) +{ + if ((cfg == NULL) || (! cfg->in_use) || (tile == NULL) || (tile == p_null_tile)) + return false; + + if (cfg->type == TAT_COASTAL_WAVE) { + enum direction dir = DIR_ZERO; + if (! get_tile_animation_coastal_wave_direction (tile_x, tile_y, &dir)) + return false; + } + + if (is->tile_animation_selected_valid && + (is->tile_animation_selected_mask_matrix != NULL) && + (tile_x >= 0) && (tile_y >= 0) && + (tile_x < p_bic_data->Map.Width) && + (tile_y < p_bic_data->Map.Height)) { + int tile_index = tile_coords_to_index (&p_bic_data->Map, tile_x, tile_y); + int animation_index = cfg - &is->tile_animation_configs[0]; + if ((tile_index >= 0) && + (tile_index < is->tile_animation_selected_tile_count) && + (animation_index >= 0) && + (animation_index < is->tile_animation_count)) { + int words_per_tile = (MAX_TILE_ANIMATION_CONFIGS + 31) / 32; + unsigned int * tile_mask = is->tile_animation_selected_mask_matrix + tile_index * words_per_tile; + bool selected = (tile_mask[animation_index / 32] & (1u << (animation_index % 32))) != 0; + if (for_draw) + return selected; + } + } + + (void)for_draw; + return tile_animation_matches_time_filters (cfg) && + tile_animation_rule_matches_tile_base (cfg, tile, tile_x, tile_y); +} + +bool +tile_has_matching_resource_animation_for_draw (Tile * tile, int tile_x, int tile_y) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + int resource_id = tile->vtable->m39_Get_Resource_Type (tile); + return tile_has_matching_resource_animation_for_draw_with_resource (tile, tile_x, tile_y, resource_id, NULL); +} + +bool +tile_has_matching_resource_animation_for_draw_with_resource (Tile * tile, int tile_x, int tile_y, int resource_id, int * out_effect_id) +{ + if (out_effect_id != NULL) + *out_effect_id = -1; + + if (! is->current_config.enable_custom_animations) + return false; + if ((tile == NULL) || (tile == p_null_tile)) + return false; + if ((resource_id < 0) || (resource_id >= p_bic_data->ResourceTypeCount)) + return false; + if (Tile_has_city (tile)) + return false; + + int matched_animation_index = -1; + for (int i = 0; i < is->tile_animation_count; i++) { + struct tile_animation_config const * cfg = &is->tile_animation_configs[i]; + if ((cfg == NULL) || (! cfg->in_use)) + continue; + if ((cfg->type != TAT_RESOURCE) || (cfg->resource_id != resource_id)) + continue; + if (! tile_animation_matches_time_filters (cfg)) + continue; + + bool adjacent_ok = true; + if (cfg->adjacent_to_count > 0) { + adjacent_ok = false; + for (int k = 0; k < cfg->adjacent_to_count; k++) + if (tile_animation_adjacent_requirement_matches (&cfg->adjacent_to[k], tile_x, tile_y)) { + adjacent_ok = true; + break; + } + } + if (! adjacent_ok) + continue; + + if (matched_animation_index < i) + matched_animation_index = i; + } + + if (matched_animation_index < 0) + return false; + if (out_effect_id != NULL) + *out_effect_id = is->tile_animation_configs[matched_animation_index].effect_id; + return true; +} + +void +reset_tile_animation_runtime_state () +{ + free_tile_animation_selected_matrix (); + if (is->tile_destruct_animation_ages != NULL) { + free (is->tile_destruct_animation_ages); + is->tile_destruct_animation_ages = NULL; + } + clear_tile_animation_pcx_sprite_lookup (); + refresh_tile_animation_pcx_rule_mask (); + is->tile_animation_spawn_effect_override = 0; + is->tile_animation_spawn_effect_override_active = false; +} + +void +clear_tile_animation_configs () +{ + for (int i = 0; i < is->tile_animation_count; i++) { + struct tile_animation_config * cfg = &is->tile_animation_configs[i]; + if (cfg->name != NULL) + free ((void *)cfg->name); + if (cfg->ini_path != NULL) + free ((void *)cfg->ini_path); + memset (cfg, 0, sizeof *cfg); + } + is->tile_animation_count = 0; + reset_tile_animation_runtime_state (); +} + +void +init_parsed_tile_animation_definition (struct parsed_tile_animation_definition * def) +{ + memset (def, 0, sizeof *def); + def->type = TAT_TERRAIN; + def->terrain_types_mask = square_type_mask_bit (SQ_Grassland); + def->natural_wonder_id = -1; + def->district_id = -1; + def->pcx_file_id = TILE_ANIM_PCX_FILE_UNKNOWN; + def->pcx_index = -1; +} + +void +free_parsed_tile_animation_definition (struct parsed_tile_animation_definition * def) +{ + if (def->name != NULL) + free (def->name); + if (def->ini_path != NULL) + free (def->ini_path); + if (def->resource_type != NULL) + free (def->resource_type); + if (def->pcx_file != NULL) + free (def->pcx_file); + init_parsed_tile_animation_definition (def); +} + +bool +parse_tile_animation_hour_list (struct string_slice const * value, unsigned int * out_mask) +{ + char * text = extract_slice (value); + if (text == NULL) + return false; + + unsigned int mask = 0; + char * cursor = text; + while (*cursor != '\0') { + while ((*cursor == ' ') || (*cursor == '\t') || (*cursor == ',')) + cursor++; + if (*cursor == '\0') + break; + + if ((*cursor < '0') || (*cursor > '9')) { + free (text); + return false; + } + int start_hour = 0; + while ((*cursor >= '0') && (*cursor <= '9')) { + start_hour = start_hour * 10 + (*cursor - '0'); + cursor++; + } + if ((start_hour < 0) || (start_hour > 23)) { + free (text); + return false; + } + + int end_hour = start_hour; + bool has_range = false; + while ((*cursor == ' ') || (*cursor == '\t')) + cursor++; + if (*cursor == '-') { + has_range = true; + cursor++; + while ((*cursor == ' ') || (*cursor == '\t')) + cursor++; + if ((*cursor < '0') || (*cursor > '9')) { + free (text); + return false; + } + end_hour = 0; + while ((*cursor >= '0') && (*cursor <= '9')) { + end_hour = end_hour * 10 + (*cursor - '0'); + cursor++; + } + if ((end_hour < 0) || (end_hour > 23)) { + free (text); + return false; + } + } + if (! has_range || (end_hour >= start_hour)) { + for (int hour = start_hour; hour <= end_hour; hour++) + mask |= (1u << hour); + } else { + // Wraparound range, e.g. 18-5 => 18..23 plus 0..5. + for (int hour = start_hour; hour <= 23; hour++) + mask |= (1u << hour); + for (int hour = 0; hour <= end_hour; hour++) + mask |= (1u << hour); + } + + while ((*cursor == ' ') || (*cursor == '\t')) + cursor++; + if (*cursor == ',') + cursor++; + else if (*cursor != '\0') { + free (text); + return false; + } + } + + free (text); + *out_mask = mask; + return true; +} + +bool +parse_tile_animation_season_list (struct string_slice const * value, unsigned int * out_mask) +{ + char * text = extract_slice (value); + if (text == NULL) + return false; + + unsigned int mask = 0; + char * cursor = text; + while (*cursor != '\0') { + while ((*cursor == ' ') || (*cursor == '\t') || (*cursor == ',')) + cursor++; + if (*cursor == '\0') + break; + + char * start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; + struct string_slice token = { .str = start, .len = cursor - start }; + token = trim_string_slice (&token, 1); + + if (slice_matches_str (&token, "spring")) + mask |= 1u << CS_SPRING; + else if (slice_matches_str (&token, "summer")) + mask |= 1u << CS_SUMMER; + else if (slice_matches_str (&token, "fall")) + mask |= 1u << CS_FALL; + else if (slice_matches_str (&token, "winter")) + mask |= 1u << CS_WINTER; + else { + free (text); + return false; + } + + if (*cursor == ',') + cursor++; + } + + free (text); + *out_mask = mask; + return true; +} + +bool +parse_tile_animation_culture_group_list (struct string_slice const * value, unsigned int * out_mask) +{ + char * text = extract_slice (value); + if (text == NULL) + return false; + + unsigned int mask = 0; + char * cursor = text; + while (*cursor != '\0') { + while ((*cursor == ' ') || (*cursor == '\t') || (*cursor == ',')) + cursor++; + if (*cursor == '\0') + break; + + char * start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; + struct string_slice token = { .str = start, .len = cursor - start }; + token = trim_string_slice (&token, 1); + + int culture_id = -1; + if (! find_civ_culture_id_by_name (&token, &culture_id)) { + free (text); + return false; + } + if ((culture_id < 0) || (culture_id >= 32)) { + free (text); + return false; + } + mask |= 1u << culture_id; + + if (*cursor == ',') + cursor++; + } + + free (text); + *out_mask = mask; + return true; +} + +bool +parse_tile_animation_era_list (struct string_slice const * value, unsigned int * out_mask) +{ + char * text = extract_slice (value); + if (text == NULL) + return false; + + unsigned int mask = 0; + char * cursor = text; + while (*cursor != '\0') { + while ((*cursor == ' ') || (*cursor == '\t') || (*cursor == ',')) + cursor++; + if (*cursor == '\0') + break; + + char * start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; + struct string_slice token = { .str = start, .len = cursor - start }; + token = trim_string_slice (&token, 1); + + int era_id = -1; + struct string_slice era_int = token; + if (read_int (&era_int, &era_id)) { + if ((era_id < 0) || (era_id > 3)) { + free (text); + return false; + } + } else if (slice_matches_str (&token, "ancient") || slice_matches_str (&token, "ancient times")) { + era_id = 0; + } else if (slice_matches_str (&token, "middle") || slice_matches_str (&token, "middle ages") || slice_matches_str (&token, "medieval")) { + era_id = 1; + } else if (slice_matches_str (&token, "industrial") || slice_matches_str (&token, "industrial ages")) { + era_id = 2; + } else if (slice_matches_str (&token, "modern") || slice_matches_str (&token, "modern times")) { + era_id = 3; + } else { + free (text); + return false; + } + mask |= 1u << era_id; + + if (*cursor == ',') + cursor++; + } + + free (text); + *out_mask = mask; + return true; +} + +bool +parse_tile_animation_adjacent_to (struct string_slice const * value, + struct tile_animation_adjacent_requirement * out_reqs, + int * out_count) +{ + char * text = extract_slice (value); + if (text == NULL) + return false; + + int count = 0; + char * cursor = text; + while (*cursor != '\0') { + while ((*cursor == ' ') || (*cursor == '\t') || (*cursor == ',')) + cursor++; + if (*cursor == '\0') + break; + + char * token_start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; + struct string_slice token = { .str = token_start, .len = cursor - token_start }; + token = trim_string_slice (&token, 1); + if (token.len <= 0) { + if (*cursor == ',') + cursor++; + continue; + } + + char * colon = NULL; + for (int i = 0; i < token.len; i++) + if (token.str[i] == ':') { + colon = token.str + i; + break; + } + + struct string_slice terrain_token = token; + struct string_slice dir_token = {0}; + if (colon != NULL) { + terrain_token.len = colon - terrain_token.str; + dir_token.str = colon + 1; + dir_token.len = token.len - (terrain_token.len + 1); + dir_token = trim_string_slice (&dir_token, 1); + } + terrain_token = trim_string_slice (&terrain_token, 1); + + if ((count < MAX_TILE_ANIMATION_ADJACENCY) && (terrain_token.len > 0)) { + struct tile_animation_adjacent_requirement * req = &out_reqs[count]; + memset (req, 0, sizeof *req); + + if (slice_matches_str (&terrain_token, "land")) { + req->is_land = true; + req->square_type = SQ_Grassland; + } else if (! read_tile_terrain_type_value (&terrain_token, &req->square_type)) { + free (text); + return false; + } + + if (dir_token.len > 0) { + if (! read_direction_value (&dir_token, &req->direction)) { + free (text); + return false; + } + req->has_direction = true; + } + count++; + } + + if (*cursor == ',') + cursor++; + } + + free (text); + *out_count = count; + return true; +} + +int +find_tile_animation_index_by_name (char const * name) +{ + if ((name == NULL) || (name[0] == '\0')) + return -1; + for (int i = 0; i < is->tile_animation_count; i++) { + char const * existing = is->tile_animation_configs[i].name; + if ((existing != NULL) && (strcmp (existing, name) == 0)) + return i; + } + return -1; +} + +bool +add_tile_animation_from_definition (struct parsed_tile_animation_definition * def) +{ + if ((def == NULL) || (! def->has_name) || (def->name == NULL)) + return false; + + int existing = find_tile_animation_index_by_name (def->name); + int dest = (existing >= 0) ? existing : is->tile_animation_count; + if ((dest < 0) || (dest >= MAX_TILE_ANIMATION_CONFIGS)) + return false; + + struct tile_animation_config cfg; + memset (&cfg, 0, sizeof cfg); + cfg.name = strdup (def->name); + cfg.ini_path = strdup (def->ini_path); + cfg.type = def->type; + cfg.terrain_types_mask = def->terrain_types_mask; + cfg.terrain_types_include_land = def->terrain_types_include_land; + cfg.natural_wonder_id = def->natural_wonder_id; + cfg.district_id = def->district_id; + cfg.pcx_file_id = def->pcx_file_id; + cfg.pcx_index = def->pcx_index; + cfg.direction = def->direction; + cfg.x_offset = def->x_offset; + cfg.y_offset = def->y_offset; + cfg.frame_time_seconds = def->frame_time_seconds; + cfg.has_direction = def->has_direction; + cfg.has_x_offset = def->has_x_offset; + cfg.has_y_offset = def->has_y_offset; + cfg.has_frame_time_seconds = def->has_frame_time_seconds; + cfg.day_night_hour_mask = def->day_night_hour_mask; + cfg.season_mask = def->season_mask; + cfg.culture_group_mask = def->culture_group_mask; + cfg.era_mask = def->era_mask; + cfg.adjacent_to_count = def->adjacent_to_count; + for (int i = 0; i < def->adjacent_to_count; i++) + cfg.adjacent_to[i] = def->adjacent_to[i]; + cfg.resource_id = -1; + if (cfg.type == TAT_RESOURCE) { + struct string_slice resource_name = {.str = def->resource_type, .len = strlen (def->resource_type)}; + if (! find_resource_id_by_name (&resource_name, &cfg.resource_id)) { + free ((void *)cfg.name); + free ((void *)cfg.ini_path); + return false; + } + } else if (cfg.type == TAT_PCX) { + if ((cfg.pcx_file_id < 0) || (cfg.pcx_index < 0)) { + free ((void *)cfg.name); + free ((void *)cfg.ini_path); + return false; + } + } else if ((cfg.type == TAT_DESTRUCT_INITIAL) || (cfg.type == TAT_DESTRUCT_AFTER)) { + // No extra required fields. + } else if (cfg.type == TAT_COASTAL_WAVE) { + // No extra required fields. + } + + cfg.effect_id = is->tile_animation_effect_base + dest; + cfg.in_use = true; + + if (existing >= 0) { + struct tile_animation_config * old = &is->tile_animation_configs[existing]; + if (old->name != NULL) + free ((void *)old->name); + if (old->ini_path != NULL) + free ((void *)old->ini_path); + *old = cfg; + } else { + is->tile_animation_configs[dest] = cfg; + is->tile_animation_count = dest + 1; + } + refresh_tile_animation_pcx_rule_mask (); + return true; +} + +void +finalize_parsed_tile_animation_definition (struct parsed_tile_animation_definition * def, + int section_start_line, + struct error_line ** parse_errors) +{ + bool ok = true; + if (! def->has_name) { + ok = false; + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: name (value is required)", section_start_line); + } + if (! def->has_ini_path) { + ok = false; + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: ini_path (value is required)", section_start_line); + } + if (! def->has_type) { + ok = false; + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: type (value is required)", section_start_line); + } + if (def->type == TAT_RESOURCE && ! def->has_resource_type) { + ok = false; + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: resource_type (value is required for type=resource)", section_start_line); + } + if (def->type == TAT_PCX && ! def->has_pcx_file) { + ok = false; + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: pcx_file (value is required for type=pcx)", section_start_line); + } + if (def->type == TAT_PCX && ! def->has_pcx_index) { + ok = false; + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: pcx_index (value is required for type=pcx)", section_start_line); + } + if (def->type == TAT_TERRAIN && ! def->has_terrain_types) { + ok = false; + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: terrain_types (value is required for type=terrain)", section_start_line); + } + if (ok && ! add_tile_animation_from_definition (def)) { + struct error_line * e = add_error_line (parse_errors); + snprintf (e->text, sizeof e->text, "^ Line %d: failed to add animation entry", section_start_line); + } + free_parsed_tile_animation_definition (def); +} + +void +handle_tile_animation_definition_key (struct parsed_tile_animation_definition * def, + struct string_slice const * key, + struct string_slice const * value, + int line_number, + struct error_line ** parse_errors, + struct error_line ** unrecognized_keys) +{ + if (slice_matches_str (key, "name")) { + if (def->name != NULL) free (def->name); + struct string_slice v = trim_string_slice (value, 1); + if (v.len <= 0) { + def->has_name = false; + add_key_parse_error (parse_errors, line_number, key, value, "(value is required)"); + } else { + def->name = extract_slice (&v); + def->has_name = def->name != NULL; + } + } else if (slice_matches_str (key, "ini_path")) { + if (def->ini_path != NULL) free (def->ini_path); + struct string_slice v = trim_string_slice (value, 1); + if (v.len <= 0) { + def->has_ini_path = false; + add_key_parse_error (parse_errors, line_number, key, value, "(value is required)"); + } else { + def->ini_path = extract_slice (&v); + def->has_ini_path = def->ini_path != NULL; + } + } else if (slice_matches_str (key, "type")) { + struct string_slice v = trim_string_slice (value, 1); + if (slice_matches_str (&v, "terrain")) { + def->type = TAT_TERRAIN; + def->has_type = true; + } else if (slice_matches_str (&v, "resource")) { + def->type = TAT_RESOURCE; + def->has_type = true; + } else if (slice_matches_str (&v, "pcx")) { + def->type = TAT_PCX; + def->has_type = true; + } else if (slice_matches_str (&v, "destruct-initial")) { + def->type = TAT_DESTRUCT_INITIAL; + def->has_type = true; + } else if (slice_matches_str (&v, "destruct-after")) { + def->type = TAT_DESTRUCT_AFTER; + def->has_type = true; + } else if (slice_matches_str (&v, "coastal-wave")) { + def->type = TAT_COASTAL_WAVE; + def->has_type = true; + } else { + def->has_type = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected \"terrain\", \"resource\", \"pcx\", \"destruct-initial\", \"destruct-after\", or \"coastal-wave\")"); + } + } else if (slice_matches_str (key, "resource_type")) { + if (def->resource_type != NULL) free (def->resource_type); + struct string_slice v = trim_string_slice (value, 1); + def->resource_type = extract_slice (&v); + def->has_resource_type = (def->resource_type != NULL) && (def->resource_type[0] != '\0'); + } else if (slice_matches_str (key, "pcx_file")) { + if (def->pcx_file != NULL) free (def->pcx_file); + def->pcx_file = NULL; + def->pcx_file_id = TILE_ANIM_PCX_FILE_UNKNOWN; + if (read_tile_animation_pcx_file (value, &def->pcx_file_id)) { + struct string_slice v = trim_string_slice (value, 1); + def->pcx_file = extract_slice (&v); + def->has_pcx_file = true; + } else { + def->has_pcx_file = false; + add_key_parse_error (parse_errors, line_number, key, value, "(unsupported pcx_file)"); + } + } else if (slice_matches_str (key, "pcx_index")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival) && (ival >= 0) && (ival <= 4095)) { + def->pcx_index = ival; + def->has_pcx_index = true; + } else { + def->has_pcx_index = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer 0..4095)"); + } + } else if (slice_matches_str (key, "terrain_types")) { + unsigned int terrain_types_mask = 0; + bool include_land = false; + if (read_tile_animation_terrain_types (value, &terrain_types_mask, &include_land)) { + def->terrain_types_mask = terrain_types_mask; + def->terrain_types_include_land = include_land; + def->has_terrain_types = true; + } else { + def->has_terrain_types = false; + add_key_parse_error (parse_errors, line_number, key, value, "(unrecognized terrain type list)"); + } + } else if (slice_matches_str (key, "direction")) { + enum direction dir; + if (read_tile_animation_direction_value (value, &dir)) { + def->direction = dir; + def->has_direction = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(unrecognized direction)"); + } else if (slice_matches_str (key, "x_offset")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->x_offset = ival; + def->has_x_offset = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } else if (slice_matches_str (key, "y_offset")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->y_offset = ival; + def->has_y_offset = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } else if (slice_matches_str (key, "frame_time_seconds")) { + float fval; + if (read_float (value, &fval)) { + def->frame_time_seconds = fval; + def->has_frame_time_seconds = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected float)"); + } else if (slice_matches_str (key, "adjacent_to")) { + if (parse_tile_animation_adjacent_to (value, def->adjacent_to, &def->adjacent_to_count)) + def->has_adjacent_to = true; + else + add_key_parse_error (parse_errors, line_number, key, value, "(unrecognized adjacent_to value)"); + } else if (slice_matches_str (key, "show_in_day_night_hours")) { + unsigned int mask = 0; + if (parse_tile_animation_hour_list (value, &mask)) { def->day_night_hour_mask = mask; def->has_day_night_hour_mask = true; } + else add_key_parse_error (parse_errors, line_number, key, value, "(expected comma-delimited 0..23 hour list)"); + } else if (slice_matches_str (key, "show_in_seasons")) { + unsigned int mask = 0; + if (parse_tile_animation_season_list (value, &mask)) { def->season_mask = mask; def->has_season_mask = true; } + else add_key_parse_error (parse_errors, line_number, key, value, "(expected comma-delimited season list)"); + } else + add_unrecognized_key_error (unrecognized_keys, line_number, key); +} + +void +load_tile_animation_config_file (char const * file_path, int path_is_relative_to_mod_dir, int log_missing, int drop_existing_configs) +{ + char path[MAX_PATH]; + if (path_is_relative_to_mod_dir) { + if (is->mod_rel_dir == NULL) + return; + snprintf (path, sizeof path, "%s\\%s", is->mod_rel_dir, file_path); + } else + strncpy (path, file_path, sizeof path); + path[(sizeof path) - 1] = '\0'; + + char * text = file_to_string (path); + if (text == NULL) { + if (log_missing) { + char ss[256]; + snprintf (ss, sizeof ss, "[C3X] Tile animations config file not found: %s", path); + (*p_OutputDebugStringA) (ss); + } + return; + } + + if (drop_existing_configs) + clear_tile_animation_configs (); + snprintf (is->current_tile_animations_config_path, sizeof is->current_tile_animations_config_path, path); + + struct parsed_tile_animation_definition def; + init_parsed_tile_animation_definition (&def); + bool in_section = false; + int section_start_line = 0; + int line_number = 0; + struct error_line * parse_errors = NULL; + struct error_line * unrecognized_keys = NULL; + + char * cursor = text; + while (*cursor != '\0') { + line_number++; + char * line_start = cursor; + char * line_end = cursor; + while ((*line_end != '\0') && (*line_end != '\n')) + line_end++; + bool has_newline = (*line_end == '\n'); + if (has_newline) + *line_end = '\0'; + struct string_slice line = { .str = line_start, .len = line_end - line_start }; + struct string_slice trimmed = trim_string_slice (&line, 0); + if (line_is_empty_or_comment (&trimmed)) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } + + if (trimmed.str[0] == '#') { + struct string_slice directive = trimmed; + directive.str++; + directive.len--; + directive = trim_string_slice (&directive, 0); + if ((directive.len > 0) && slice_matches_str (&directive, "Animation")) { + if (in_section) + finalize_parsed_tile_animation_definition (&def, section_start_line, &parse_errors); + in_section = true; + section_start_line = line_number; + } + cursor = has_newline ? line_end + 1 : line_end; + continue; + } + + if (! in_section) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } + + struct string_slice key = {0}, value = {0}; + enum key_value_parse_status status = parse_trimmed_key_value (&trimmed, &key, &value); + if (status == KVP_NO_EQUALS) { + char * line_text = extract_slice (&trimmed); + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected '=')", line_number, line_text); + free (line_text); + cursor = has_newline ? line_end + 1 : line_end; + continue; + } else if (status == KVP_EMPTY_KEY) { + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: (missing key)", line_number); + cursor = has_newline ? line_end + 1 : line_end; + continue; + } + + handle_tile_animation_definition_key (&def, &key, &value, line_number, &parse_errors, &unrecognized_keys); + cursor = has_newline ? line_end + 1 : line_end; + } + + if (in_section) + finalize_parsed_tile_animation_definition (&def, section_start_line, &parse_errors); + free_parsed_tile_animation_definition (&def); + free (text); + + struct loaded_config_name * top_lcn = is->loaded_config_names; + while (top_lcn->next != NULL) + top_lcn = top_lcn->next; + struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); + new_lcn->name = strdup (path); + new_lcn->next = NULL; + top_lcn->next = new_lcn; + + if ((parse_errors != NULL) || (unrecognized_keys != NULL)) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); + char s[200]; + snprintf (s, sizeof s, "Tile animation config errors in %s:", path); + PopupForm_add_text (popup, __, s, false); + for (struct error_line * line = parse_errors; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); + if (unrecognized_keys != NULL) { + PopupForm_add_text (popup, __, "", false); + PopupForm_add_text (popup, __, "Unrecognized keys:", false); + for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); + } + patch_show_popup (popup, __, 0, 0); + } + free_error_lines (parse_errors); + free_error_lines (unrecognized_keys); +} + +void +copy_animation_entry_to_tile_animation_config (struct tile_animation_config * cfg, + struct natural_wonder_animation_config const * anim) +{ + if ((cfg == NULL) || (anim == NULL)) + return; + + cfg->ini_path = strdup (anim->ini_path); + if (cfg->ini_path == NULL) + return; + cfg->day_night_hour_mask = anim->day_night_hour_mask; + cfg->season_mask = anim->season_mask; + cfg->culture_group_mask = anim->culture_group_mask; + cfg->era_mask = anim->era_mask; + if (anim->has_direction) { + cfg->direction = anim->direction; + cfg->has_direction = true; + } + if (anim->has_frame_time_seconds) { + cfg->frame_time_seconds = anim->frame_time_seconds; + cfg->has_frame_time_seconds = true; + } + if (anim->has_offsets) { + cfg->x_offset = anim->x_offset; + cfg->y_offset = anim->y_offset; + cfg->has_x_offset = true; + cfg->has_y_offset = true; + } +} + +void +add_natural_wonder_tile_animation_configs () +{ + if (! is->current_config.enable_natural_wonders) + return; + + for (int wonder_id = 0; wonder_id < is->natural_wonder_count; wonder_id++) { + struct natural_wonder_district_config const * nw = &is->natural_wonder_configs[wonder_id]; + for (int i = 0; i < nw->animation_count; i++) { + struct natural_wonder_animation_config const * anim = &nw->animations[i]; + if ((anim->ini_path == NULL) || (anim->ini_path[0] == '\0')) + continue; + if (is->tile_animation_count >= MAX_TILE_ANIMATION_CONFIGS) + return; + + int dest = is->tile_animation_count++; + struct tile_animation_config * cfg = &is->tile_animation_configs[dest]; + memset (cfg, 0, sizeof *cfg); + cfg->type = TAT_NATURAL_WONDER; + cfg->natural_wonder_id = wonder_id; + copy_animation_entry_to_tile_animation_config (cfg, anim); + if (cfg->ini_path == NULL) { + is->tile_animation_count--; + continue; + } + cfg->effect_id = is->tile_animation_effect_base + dest; + cfg->in_use = true; + } + } + refresh_tile_animation_pcx_rule_mask (); +} + +void +add_district_tile_animation_configs () +{ + if (! is->current_config.enable_districts) + return; + + for (int district_id = 0; district_id < is->district_count; district_id++) { + if (district_id == NATURAL_WONDER_DISTRICT_ID) + continue; + struct district_config const * district = &is->district_configs[district_id]; + for (int i = 0; i < district->animation_count; i++) { + struct natural_wonder_animation_config const * anim = &district->animations[i]; + if ((anim->ini_path == NULL) || (anim->ini_path[0] == '\0')) + continue; + if (is->tile_animation_count >= MAX_TILE_ANIMATION_CONFIGS) + return; + + int dest = is->tile_animation_count++; + struct tile_animation_config * cfg = &is->tile_animation_configs[dest]; + memset (cfg, 0, sizeof *cfg); + cfg->type = TAT_DISTRICT; + cfg->district_id = district_id; + copy_animation_entry_to_tile_animation_config (cfg, anim); + if (cfg->ini_path == NULL) { + is->tile_animation_count--; + continue; + } + cfg->effect_id = is->tile_animation_effect_base + dest; + cfg->in_use = true; + } + } + refresh_tile_animation_pcx_rule_mask (); +} + +void +load_tile_animation_configs () +{ + if (! is->current_config.enable_custom_animations) { + clear_tile_animation_configs (); + return; + } + + if (is->tile_animation_effect_base <= 0) + is->tile_animation_effect_base = 1000; + load_tile_animation_config_file ("default.tile_animations.txt", 1, 1, 1); + load_tile_animation_config_file ("user.tile_animations.txt", 1, 0, 1); + + char * scenario_filename = "scenario.tile_animations.txt"; + char * scenario_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); + if ((scenario_path != NULL) && (0 != strcmp (scenario_filename, scenario_path))) + load_tile_animation_config_file (scenario_path, 0, 0, 1); + add_natural_wonder_tile_animation_configs (); + add_district_tile_animation_configs (); + + rebuild_tile_animation_pcx_sprite_lookup (); + rebuild_tile_animation_rule_match_cache (); +} + +int +get_tile_animation_type_priority (enum tile_animation_type type) +{ + // Higher number = stronger winner preference. + // Keep this centralized so new animation types (district/natural wonder/etc.) + // can be assigned clearly without touching scheduler logic. + switch (type) { + case TAT_RESOURCE: return 80; + case TAT_NATURAL_WONDER: return 70; + case TAT_DESTRUCT_INITIAL: return 60; + case TAT_DESTRUCT_AFTER: return 50; + case TAT_DISTRICT: return 40; + case TAT_PCX: return 30; + case TAT_TERRAIN: return 20; + case TAT_COASTAL_WAVE: return 10; + default: return 0; + } +} + +int +pick_tile_animation_winner_for_tile (unsigned int * tile_mask) +{ + if ((tile_mask == NULL) || (is->tile_animation_count <= 0)) + return -1; + + int winner = -1; + int winner_score = -1; + for (int i = 0; i < is->tile_animation_count; i++) { + if ((tile_mask[i / 32] & (1u << (i % 32))) == 0) + continue; + + struct tile_animation_config const * cfg = &is->tile_animation_configs[i]; + if ((cfg == NULL) || (! cfg->in_use)) + continue; + + int score = get_tile_animation_type_priority (cfg->type); + if (cfg->season_mask != 0) + score += 1; + if (cfg->day_night_hour_mask != 0) + score += 1; + if (cfg->culture_group_mask != 0) + score += 1; + if (cfg->era_mask != 0) + score += 1; + + // Deterministic tie-break: higher config index wins for same score. + if ((winner < 0) || (score > winner_score) || ((score == winner_score) && (i > winner))) { + winner = i; + winner_score = score; + } + } + return winner; +} + +void +tile_animation_scheduler_tick () +{ + if (! is->current_config.enable_custom_animations) + return; + // Trade_Net recompute_resources temporarily increases Map.TileCount to include synthetic + // resource tiles. Custom animation selection buffers are sized for real map tiles, so + // running the scheduler in that window can overrun those buffers. + if (is->saved_tile_count >= 0) + return; + if ((p_main_screen_form == NULL) || p_main_screen_form->is_now_loading_game) + return; + if (is->tile_animation_count <= 0) + return; + + if (tile_animation_cache_needs_rebuild ()) + rebuild_tile_animation_rule_match_cache (); + if (! is->tile_animation_selected_valid || + (is->tile_animation_selected_mask_matrix == NULL) || + (is->tile_animation_selected_next_index == NULL) || + (is->tile_animation_selected_tile_indices == NULL)) + return; + + Map * map = &p_bic_data->Map; + int tile_count = map->TileCount; + if (tile_count != is->tile_animation_selected_tile_count) + return; + + for (int n = 0; n < is->tile_animation_selected_match_count; n++) { + int tile_index = is->tile_animation_selected_tile_indices[n]; + if ((tile_index < 0) || (tile_index >= tile_count)) + continue; + int tile_x, tile_y; + tile_index_to_coords (map, tile_index, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + + int i = is->tile_animation_selected_next_index[tile_index]; + if ((i < 0) || (i >= is->tile_animation_count)) + continue; + + struct tile_animation_config * cfg = &is->tile_animation_configs[i]; + if ((cfg == NULL) || (! cfg->in_use)) + continue; + + // Keep one ambient effect per tile, except a stale temporal destruction effect + // may be replaced by the newly selected winner. + if (tile->Body.active_tile_effect != NULL) { + int active_effect_id = tile->Body.active_tile_effect->V[2]; + struct tile_animation_config * active_cfg = get_tile_animation_for_effect (active_effect_id); + if ((active_cfg != NULL) && is_tile_destruct_animation_type (active_cfg->type) && + (active_effect_id == cfg->effect_id)) + continue; + if ((active_cfg == NULL) || (! is_tile_destruct_animation_type (active_cfg->type))) + continue; + } + patch_Tile_spawn_animated_effect (tile, __, cfg->effect_id, tile_x, tile_y, true, DIR_SW); + } +} + +bool +is_custom_tile_animation_effect (int effect_id) +{ + int e = effect_id; + int base = is->tile_animation_effect_base; + return (e >= base) && (e < base + is->tile_animation_count); +} + +struct tile_animation_config * +get_tile_animation_for_effect (int effect_id) +{ + if (! is_custom_tile_animation_effect (effect_id)) + return NULL; + int idx = effect_id - is->tile_animation_effect_base; + if ((idx < 0) || (idx >= is->tile_animation_count)) + return NULL; + return &is->tile_animation_configs[idx]; +} + +void __stdcall +patch_on_timer_0x9F6500 (void) +{ + if (is->current_config.enable_custom_animations && + ((*p_debug_mode_bits & 0xC) == 0)) + tile_animation_scheduler_tick (); + on_timer_0x9F6500 (); +} + +void __fastcall +patch_Units_Image_Data_load_animated_effect (Units_Image_Data * this, int edx, FLC_Animation * anim, int effect_id) +{ + if (! is->current_config.enable_custom_animations) { + Units_Image_Data_load_animated_effect (this, __, anim, effect_id); + return; + } + + int cfg_effect_id = effect_id; + if (is->tile_animation_spawn_effect_override_active) + cfg_effect_id = is->tile_animation_spawn_effect_override; + struct tile_animation_config * cfg = get_tile_animation_for_effect (cfg_effect_id); + if ((cfg == NULL) || (cfg->ini_path == NULL) || (cfg->ini_path[0] == '\0')) { + Units_Image_Data_load_animated_effect (this, __, anim, effect_id); + return; + } + + char rel_art_path[MAX_PATH]; + snprintf (rel_art_path, sizeof rel_art_path, "Animations\\%s", cfg->ini_path); + rel_art_path[(sizeof rel_art_path) - 1] = '\0'; + + char asset_path[MAX_PATH]; + get_mod_art_path (rel_art_path, asset_path, sizeof asset_path); + asset_path[(sizeof asset_path) - 1] = '\0'; + + Units_Image_Data_load_animation (this, __, asset_path, anim, 0, -1, 1, true); + if ((anim == NULL) || (anim->Animation_Info == NULL)) { + Units_Image_Data_load_animated_effect (this, __, anim, effect_id); + return; + } + + float frame_time_seconds = cfg->has_frame_time_seconds ? cfg->frame_time_seconds : 0.15f; + if (anim->Animation_Info->anim_frame_time_seconds != NULL) + anim->Animation_Info->anim_frame_time_seconds[AT_ATTACK1] = frame_time_seconds; +} +void __fastcall +patch_Tile_spawn_animated_effect (Tile * this, int edx, enum AnimatedEffect effect, int tile_x, int tile_y, bool randomize_start_frame, enum direction dummy_dir) +{ + if (is->current_config.enable_custom_animations && is_custom_tile_animation_effect (effect)) { + struct tile_animation_config * cfg = get_tile_animation_for_effect (effect); + struct district_instance * inst = get_district_instance (this); + if (Tile_has_city (this)) + return; + if (inst != NULL) { + bool allow_natural_wonder_tile = (cfg != NULL) && (cfg->type == TAT_NATURAL_WONDER) && + (inst->district_id == NATURAL_WONDER_DISTRICT_ID) && + ((cfg->natural_wonder_id < 0) || (inst->natural_wonder_info.natural_wonder_id == cfg->natural_wonder_id)); + bool allow_district_tile = (cfg != NULL) && (cfg->type == TAT_DISTRICT) && + (inst->district_id != NATURAL_WONDER_DISTRICT_ID) && + ((cfg->district_id < 0) || (inst->district_id == cfg->district_id)); + bool allow_resource_on_district_tile = (cfg != NULL) && (cfg->type == TAT_RESOURCE); + bool allow_destruct_tile = (cfg != NULL) && is_tile_destruct_animation_type (cfg->type); + if (! allow_natural_wonder_tile && ! allow_district_tile && ! allow_resource_on_district_tile && ! allow_destruct_tile) + return; + } + enum direction effective_direction = DIR_ZERO; + bool has_effective_direction = false; + if (cfg != NULL) { + if (cfg->type == TAT_COASTAL_WAVE) { + if (! get_tile_animation_coastal_wave_direction (tile_x, tile_y, &effective_direction)) + return; + has_effective_direction = true; + } else if (cfg->has_direction) { + effective_direction = cfg->direction; + has_effective_direction = true; + } + } + int prev_override = is->tile_animation_spawn_effect_override; + bool had_override = is->tile_animation_spawn_effect_override_active; + is->tile_animation_spawn_effect_override = effect; + is->tile_animation_spawn_effect_override_active = true; + Tile_spawn_animated_effect (this, __, AE_Disorder, tile_x, tile_y, randomize_start_frame, dummy_dir); + + // Optional per-effect direction and pixel offsets after vanilla centers the animation on the tile. + // Positive X moves right, positive Y moves down. + Tile_Animated_Effect * fx = this->Body.active_tile_effect; + if ((fx != NULL) && (cfg != NULL)) { + fx->V[2] = effect; + if (has_effective_direction) { + fx->flc_animation.summary.direction = effective_direction; + fx->flc_animation.summary.direction_2 = effective_direction; + } + int x_off = cfg->has_x_offset ? cfg->x_offset : 0; + int y_off = cfg->has_y_offset ? cfg->y_offset : 0; + fx->flc_animation.summary.pixel_loc_x += x_off; + fx->flc_animation.summary.pixel_loc_y += y_off; + fx->flc_animation.summary.pixel_target_x += x_off; + fx->flc_animation.summary.pixel_target_y += y_off; + } + + is->tile_animation_spawn_effect_override = prev_override; + is->tile_animation_spawn_effect_override_active = had_override; + return; + } + Tile_spawn_animated_effect (this, __, effect, tile_x, tile_y, randomize_start_frame, dummy_dir); +} // TCC requires a main function be defined even though it's never used. int main () { return 0; }