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..8bfd84ba --- /dev/null +++ b/Art/Animations/Districts/WindFarm/WindFarm.INI @@ -0,0 +1,91 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=Districts\Windfarm\WindFarm.flc +WALK= +RUN= +ATTACK1=Districts\Windfarm\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..27ae9eda --- /dev/null +++ b/Art/Animations/Districts/WindFarm/WindFarm_night.INI @@ -0,0 +1,91 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=Districts\Windfarm\WindFarm_night.flc +WALK= +RUN= +ATTACK1=Districts\Windfarm\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/AngelFalls.INI b/Art/Animations/NaturalWonders/AngelFalls/AngelFalls.INI new file mode 100644 index 00000000..dbdcf34a --- /dev/null +++ b/Art/Animations/NaturalWonders/AngelFalls/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/AngelFalls.flc b/Art/Animations/NaturalWonders/AngelFalls/AngelFalls.flc new file mode 100644 index 00000000..5782838a Binary files /dev/null and b/Art/Animations/NaturalWonders/AngelFalls/AngelFalls.flc differ diff --git a/Art/Animations/NaturalWonders/AngelFalls/AngelFalls_night.INI b/Art/Animations/NaturalWonders/AngelFalls/AngelFalls_night.INI new file mode 100644 index 00000000..ab32fe25 --- /dev/null +++ b/Art/Animations/NaturalWonders/AngelFalls/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/AngelFalls_night.flc b/Art/Animations/NaturalWonders/AngelFalls/AngelFalls_night.flc new file mode 100644 index 00000000..307071cf Binary files /dev/null and b/Art/Animations/NaturalWonders/AngelFalls/AngelFalls_night.flc differ diff --git a/Art/Animations/NaturalWonders/Yellowstone/Yellowstone.INI b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone.INI new file mode 100644 index 00000000..4da64c91 --- /dev/null +++ b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=170 +Fast Speed=170 + +[Animations] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1=Yellowstone.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/Yellowstone/Yellowstone.flc b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone.flc new file mode 100644 index 00000000..ec4dd5ff Binary files /dev/null and b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone.flc differ diff --git a/Art/Animations/NaturalWonders/Yellowstone/Yellowstone_night.INI b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone_night.INI new file mode 100644 index 00000000..260fbcba --- /dev/null +++ b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone_night.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=170 +Fast Speed=170 + +[Animations] +BLANK= +DEFAULT= +WALK= +RUN= +ATTACK1=Yellowstone_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/Yellowstone/Yellowstone_night.flc b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone_night.flc new file mode 100644 index 00000000..38a63b20 Binary files /dev/null and b/Art/Animations/NaturalWonders/Yellowstone/Yellowstone_night.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_night.flc b/Art/Animations/Resources/Cow/black and white cow_night.flc new file mode 100644 index 00000000..944efc14 Binary files /dev/null and b/Art/Animations/Resources/Cow/black and white cow_night.flc differ 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/black_and_white_cow_night.INI b/Art/Animations/Resources/Cow/black_and_white_cow_night.INI new file mode 100644 index 00000000..b8df52c3 --- /dev/null +++ b/Art/Animations/Resources/Cow/black_and_white_cow_night.INI @@ -0,0 +1,91 @@ +[Speed] +Normal Speed=225 +Fast Speed=225 + +[Animations] +BLANK= +DEFAULT=black and white cow_night.flc +WALK= +RUN= +ATTACK1=black and white cow_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/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_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_night.flc b/Art/Animations/Resources/Elephant/Elephant_night.flc new file mode 100644 index 00000000..9bc5295b Binary files /dev/null and b/Art/Animations/Resources/Elephant/Elephant_night.flc differ diff --git a/Art/Animations/Resources/Elephant/Elephant_night.ini b/Art/Animations/Resources/Elephant/Elephant_night.ini new file mode 100644 index 00000000..209fd995 --- /dev/null +++ b/Art/Animations/Resources/Elephant/Elephant_night.ini @@ -0,0 +1,80 @@ +[Speed] +Normal Speed=250 +Fast Speed=250 + +[Animations] +BLANK= +DEFAULT=Elephant_night.flc +WALK= +RUN= +ATTACK1=Elephant_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= +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/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..7cec3de7 Binary files /dev/null and b/Art/Animations/Resources/Fish/Fish.flc differ diff --git a/Art/Animations/Resources/Fish/Fish_large.flc b/Art/Animations/Resources/Fish/Fish_large.flc new file mode 100644 index 00000000..383fc41a Binary files /dev/null and b/Art/Animations/Resources/Fish/Fish_large.flc differ diff --git a/Art/Animations/Resources/Fish/README.txt b/Art/Animations/Resources/Fish/README.txt new file mode 100644 index 00000000..1c573317 --- /dev/null +++ b/Art/Animations/Resources/Fish/README.txt @@ -0,0 +1 @@ +Created by Bralvodrenak, slightly shrunken and adapted by instafluff. \ No newline at end of file diff --git a/Art/Animations/Resources/Fox/Fox.INI b/Art/Animations/Resources/Fox/Fox.INI new file mode 100644 index 00000000..dd35b0bf --- /dev/null +++ b/Art/Animations/Resources/Fox/Fox.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=Fox.flc +WALK= +RUN= +ATTACK1=Fox.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/Fox/Fox.flc b/Art/Animations/Resources/Fox/Fox.flc new file mode 100644 index 00000000..eacaff1f Binary files /dev/null and b/Art/Animations/Resources/Fox/Fox.flc differ diff --git a/Art/Animations/Resources/Fox/Fox_large.flc b/Art/Animations/Resources/Fox/Fox_large.flc new file mode 100644 index 00000000..ff5841b0 Binary files /dev/null and b/Art/Animations/Resources/Fox/Fox_large.flc differ diff --git a/Art/Animations/Resources/Fox/Fox_night.INI b/Art/Animations/Resources/Fox/Fox_night.INI new file mode 100644 index 00000000..71392800 --- /dev/null +++ b/Art/Animations/Resources/Fox/Fox_night.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=Fox_night.flc +WALK= +RUN= +ATTACK1=Fox_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/Resources/Fox/Fox_night.flc b/Art/Animations/Resources/Fox/Fox_night.flc new file mode 100644 index 00000000..3caf33f7 Binary files /dev/null and b/Art/Animations/Resources/Fox/Fox_night.flc differ diff --git a/Art/Animations/Resources/Fox/README.txt b/Art/Animations/Resources/Fox/README.txt new file mode 100644 index 00000000..25656423 --- /dev/null +++ b/Art/Animations/Resources/Fox/README.txt @@ -0,0 +1 @@ +Created by Bralvodrenak, slightly shrunken, reversed, and adapted by instafluff. \ No newline at end of file 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/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/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_night.INI b/Art/Animations/Terrain/Snow/Snow_night.INI new file mode 100644 index 00000000..54e3269d --- /dev/null +++ b/Art/Animations/Terrain/Snow/Snow_night.INI @@ -0,0 +1,100 @@ +[Speed] +Normal Speed=175 +Fast Speed=175 + +[Animations] +BLANK= +DEFAULT=Snow_night.flc +WALK= +RUN= +ATTACK1=Snow_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/Snow/Snow_night.flc b/Art/Animations/Terrain/Snow/Snow_night.flc new file mode 100644 index 00000000..19121281 Binary files /dev/null and b/Art/Animations/Terrain/Snow/Snow_night.flc 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/Districts/Summer/1200/WindFarm.PCX b/Art/Districts/Summer/1200/WindFarm.PCX new file mode 100644 index 00000000..33118518 Binary files /dev/null and b/Art/Districts/Summer/1200/WindFarm.PCX differ diff --git a/Art/TileHighlights_.pcx b/Art/TileHighlights_.pcx new file mode 100644 index 00000000..5793db3f Binary files /dev/null and b/Art/TileHighlights_.pcx differ diff --git a/C3X.h b/C3X.h index 5c26c8aa..0263638f 100644 --- a/C3X.h +++ b/C3X.h @@ -20,8 +20,8 @@ typedef unsigned char byte; #define USED_SPECIAL_DISTRICT_TYPES 11 #define MAX_DYNAMIC_DISTRICT_TYPES 22 #define COUNT_DISTRICT_TYPES (COUNT_SPECIAL_DISTRICT_TYPES + MAX_DYNAMIC_DISTRICT_TYPES) -#define MAX_WONDER_DISTRICT_TYPES 32 -#define MAX_NATURAL_WONDER_DISTRICT_TYPES 32 +#define MAX_WONDER_DISTRICT_TYPES 64 +#define MAX_NATURAL_WONDER_DISTRICT_TYPES 64 #define MAX_DISTRICT_VARIANT_COUNT 5 #define MAX_DISTRICT_ERA_COUNT 4 #define MAX_DISTRICT_COLUMN_COUNT 10 @@ -208,6 +208,18 @@ enum distribution_hub_yield_division_mode { DHYDM_SCALE_BY_CITY_COUNT }; +enum distribution_hub_city_selection_mode { + DHCSM_ALL_CITIES = 0, + DHCSM_SPECIFIC_CITIES +}; + +enum right_click_menu_item_id { + NAMED_TILE_MENU_ID = 0x90, + DISTRIBUTION_HUB_MENU_ALL_ID = 0x53000000, + DISTRIBUTION_HUB_MENU_SPECIFIC_ID = 0x53000001, + DISTRIBUTION_HUB_MENU_CITY_ID_BASE = 0x53000010 +}; + enum ai_distribution_hub_build_strategy { ADHBS_AUTO = 0, ADHBS_BY_CITY_COUNT @@ -326,6 +338,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; @@ -446,7 +459,10 @@ 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; @@ -780,6 +796,21 @@ enum district_type { DT_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; @@ -828,6 +859,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; @@ -956,6 +989,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 { @@ -969,6 +1004,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; @@ -1101,6 +1223,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; @@ -1293,6 +1417,8 @@ struct parsed_natural_wonder_definition { int happiness_bonus; 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; @@ -1310,6 +1436,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; @@ -1335,6 +1499,8 @@ struct distribution_hub_record { int tile_x; int tile_y; int civ_id; + int city_selection_mode; + struct table selected_city_ids; int food_yield; int shield_yield; int raw_food_yield; @@ -1578,6 +1744,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]; @@ -1592,6 +1759,10 @@ struct injected_state { bool named_tile_menu_active; int named_tile_menu_tile_x; int named_tile_menu_tile_y; + bool distribution_hub_menu_active; + int distribution_hub_menu_tile_x; + int distribution_hub_menu_tile_y; + bool distribution_hub_menu_reopen_requested; // List of temporary ints. Initializes to NULL/0/0, used with functions "memoize" and "clear_memo" int * memo; @@ -2072,8 +2243,6 @@ struct injected_state { SpriteList LM_Terrain_Images[9]; Sprite City_Images[80]; Sprite Destroyed_City_Images[3]; - Sprite Resources[36]; - Sprite ResourcesShadows[36]; Sprite Terrain_Buldings_Barbarian_Camp; Sprite Terrain_Buldings_Mines; Sprite Victory_Image; @@ -2139,6 +2308,8 @@ 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]; + Sprite * Resources; + int ResourceCount; } * cycle_imgs; // Districts @@ -2232,6 +2403,7 @@ struct district_button_image_set { Sprite distribution_hub_eaten_food_icon; Sprite distribution_hub_shield_icon_small; Sprite distribution_hub_food_icon_small; + Sprite distribution_hub_menu_icon_sentinel; int non_district_shield_icons_remaining; int corruption_shield_icons_remaining; int district_shield_icons_remaining; @@ -2268,6 +2440,36 @@ 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; + int tile_animation_selected_hour; + int tile_animation_selected_season; + 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/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/flc_zoom_out.py b/DayNight/flc_zoom_out.py new file mode 100644 index 00000000..aa8fcf96 --- /dev/null +++ b/DayNight/flc_zoom_out.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Scale the visible pixels in a Civ3 FLC down inside the original canvas. + +The output keeps the first input's Civ3 FLC dimensions, speed, palette bytes, +and most custom header tail values. When multiple inputs are provided, their +frames are concatenated within each Civ3 direction, and the output animation +length/header frame count are updated accordingly. It re-emits the animation +using the stock Civ3 resource FLC rhythm: per-direction BYTE_RUN keyframes and +DELTA_FLC animation frames. + +Pixels are resampled as palette indexes, not RGB, so the palette is unchanged. +Nearest-neighbor scaling is deliberate: it cannot introduce non-palette colors. +By default, combined animations are capped at 64 frames per direction. +""" + +import argparse +import os +import struct +from typing import List, Optional, Sequence, Tuple + +from flc_night_darkener import ( + CHUNK_BYTE_RUN, + CHUNK_COLOR_256, + CHUNK_DELTA_FLC, + Civ3Tail, + FlcDecoded, + build_flc_header_128, + decode_flc, + make_byte_run_payload, + make_color_256_payload, + make_frame_chunk, + make_subchunk, + pack_u16, +) + + +def flatten_inputs(raw_inputs: Sequence[Sequence[str]]) -> List[str]: + return [path for group in raw_inputs for path in group] + + +def parse_anchor(spec: str) -> Tuple[float, float]: + s = (spec or "").strip().lower().replace("_", "-") + named = { + "center": (0.5, 0.5), + "middle": (0.5, 0.5), + "top-left": (0.0, 0.0), + "top": (0.5, 0.0), + "top-right": (1.0, 0.0), + "left": (0.0, 0.5), + "right": (1.0, 0.5), + "bottom-left": (0.0, 1.0), + "bottom": (0.5, 1.0), + "bottom-right": (1.0, 1.0), + } + if s in named: + return named[s] + if "," in s: + a, b = s.split(",", 1) + ax = float(a) + ay = float(b) + if not (0.0 <= ax <= 1.0 and 0.0 <= ay <= 1.0): + raise argparse.ArgumentTypeError("anchor coordinates must be in the range 0..1") + return ax, ay + raise argparse.ArgumentTypeError( + "anchor must be a name like center/bottom or coordinates like 0.5,1.0" + ) + + +def frame_dimensions(frame: bytes, w: int, h: int) -> None: + if len(frame) != w * h: + raise ValueError(f"Decoded frame has {len(frame)} bytes, expected {w * h}.") + + +def choose_background_index(frames: Sequence[bytes], w: int, h: int) -> int: + """ + Pick a conservative fill index for the newly exposed border. + + Civ3 transparent/magenta backgrounds are normally around the frame edges, so + edge pixels across all decoded frames are a better signal than the full-frame + mode for sprites that occupy much of the canvas. + """ + if not frames: + return 0 + + counts = [0] * 256 + for frame in frames: + frame_dimensions(frame, w, h) + if w <= 0 or h <= 0: + continue + + top = frame[:w] + bottom = frame[(h - 1) * w:h * w] + for b in top: + counts[b] += 1 + for b in bottom: + counts[b] += 1 + + for y in range(1, max(1, h - 1)): + row = y * w + counts[frame[row]] += 1 + counts[frame[row + w - 1]] += 1 + + return max(range(256), key=lambda i: counts[i]) + + +def content_bbox(frame: bytes, w: int, h: int, background_index: int) -> Optional[Tuple[int, int, int, int]]: + frame_dimensions(frame, w, h) + min_x = w + min_y = h + max_x = -1 + max_y = -1 + for y in range(h): + row = y * w + for x in range(w): + if frame[row + x] == background_index: + continue + if x < min_x: + min_x = x + if y < min_y: + min_y = y + if x > max_x: + max_x = x + if y > max_y: + max_y = y + if max_x < min_x or max_y < min_y: + return None + return min_x, min_y, max_x + 1, max_y + 1 + + +def union_bbox(frames: Sequence[bytes], w: int, h: int, background_index: int) -> Optional[Tuple[int, int, int, int]]: + boxes = [content_bbox(fr, w, h, background_index) for fr in frames] + boxes = [box for box in boxes if box is not None] + if not boxes: + return None + return ( + min(box[0] for box in boxes), + min(box[1] for box in boxes), + max(box[2] for box in boxes), + max(box[3] for box in boxes), + ) + + +def scale_frame_about_anchor( + frame: bytes, + w: int, + h: int, + scale: float, + background_index: int, + bbox: Tuple[int, int, int, int], + anchor: Tuple[float, float], + shift_x: int, + shift_y: int, +) -> bytes: + frame_dimensions(frame, w, h) + x0, y0, x1, y1 = bbox + src_w = x1 - x0 + src_h = y1 - y0 + if src_w <= 0 or src_h <= 0: + return bytes([background_index]) * (w * h) + + dst_w = max(1, int(round(src_w * scale))) + dst_h = max(1, int(round(src_h * scale))) + + ax, ay = anchor + src_anchor_x = x0 + ax * src_w + src_anchor_y = y0 + ay * src_h + dst_x0 = int(round(src_anchor_x - ax * dst_w)) + shift_x + dst_y0 = int(round(src_anchor_y - ay * dst_h)) + shift_y + + out = bytearray([background_index]) * (w * h) + for dy in range(dst_h): + oy = dst_y0 + dy + if oy < 0 or oy >= h: + continue + sy = y0 + min(src_h - 1, int(dy * src_h / dst_h)) + src_row = sy * w + out_row = oy * w + for dx in range(dst_w): + ox = dst_x0 + dx + if ox < 0 or ox >= w: + continue + sx = x0 + min(src_w - 1, int(dx * src_w / dst_w)) + out[out_row + ox] = frame[src_row + sx] + return bytes(out) + + +def scale_frames( + frames: Sequence[bytes], + w: int, + h: int, + scale: float, + background_index: int, + per_frame_bbox: bool, + anchor: Tuple[float, float], + shift_x: int, + shift_y: int, +) -> List[bytes]: + if not (0.0 < scale <= 1.0): + raise ValueError("--scale must be greater than 0 and no greater than 1.") + if not frames: + raise ValueError("No decoded animation frames found.") + + shared_bbox = None if per_frame_bbox else union_bbox(frames, w, h, background_index) + out: List[bytes] = [] + for frame in frames: + bbox = content_bbox(frame, w, h, background_index) if per_frame_bbox else shared_bbox + if bbox is None: + out.append(bytes([background_index]) * (w * h)) + continue + out.append( + scale_frame_about_anchor( + frame=frame, + w=w, + h=h, + scale=scale, + background_index=background_index, + bbox=bbox, + anchor=anchor, + shift_x=shift_x, + shift_y=shift_y, + ) + ) + return out + + +def combined_anim_time_ms( + decoded: Sequence[FlcDecoded], + per_input_lengths: Sequence[int], + kept_frames_per_direction: int, + fallback_speed_ms: int, +) -> int: + remaining = kept_frames_per_direction + total = 0.0 + for dec, per_dir in zip(decoded, per_input_lengths): + if remaining <= 0: + break + take = min(remaining, per_dir) + if per_dir > 0 and dec.civ3.anim_time_ms > 0: + total += (dec.civ3.anim_time_ms / float(per_dir)) * take + else: + total += fallback_speed_ms * take + remaining -= take + return max(1, int(round(total))) + + +def direction_blocks(dec: FlcDecoded) -> List[List[bytes]]: + dir_count = max(1, dec.civ3.num_anims) + if len(dec.anim_frames) % dir_count != 0: + raise ValueError( + f"Decoded frame count {len(dec.anim_frames)} is not divisible by {dir_count} directions." + ) + per_dir = len(dec.anim_frames) // dir_count + return [ + list(dec.anim_frames[d * per_dir:(d + 1) * per_dir]) + for d in range(dir_count) + ] + + +def combine_decoded_inputs( + decoded: Sequence[FlcDecoded], + max_frames_per_direction: int, + *, + audit_directions: bool = False, +) -> FlcDecoded: + if not decoded: + raise ValueError("No input FLCs were decoded.") + if max_frames_per_direction < 0: + raise ValueError("--max-frames-per-direction must be 0 or greater.") + + first = decoded[0] + dir_count = max(1, first.civ3.num_anims) + first_directions = first.civ3.directions + decoded_blocks = [direction_blocks(dec) for dec in decoded] + per_input_lengths: List[int] = [] + + for i, dec in enumerate(decoded, start=1): + if dec.w != first.w or dec.h != first.h: + raise ValueError( + f"Input {i} has canvas {dec.w}x{dec.h}; expected {first.w}x{first.h}." + ) + if max(1, dec.civ3.num_anims) != dir_count: + raise ValueError( + f"Input {i} has {max(1, dec.civ3.num_anims)} directions; expected {dir_count}." + ) + if dec.civ3.directions != first_directions: + raise ValueError( + f"Input {i} has direction mask {dec.civ3.directions}; expected {first_directions}." + ) + if len(decoded_blocks[i - 1]) != dir_count: + raise ValueError( + f"Input {i} decoded into {len(decoded_blocks[i - 1])} direction blocks; expected {dir_count}." + ) + per_input_lengths.append(len(decoded_blocks[i - 1][0])) + + total_anim_length = sum(per_input_lengths) + if max_frames_per_direction > 0: + kept_anim_length = min(total_anim_length, max_frames_per_direction) + else: + kept_anim_length = total_anim_length + + combined: List[bytes] = [] + for direction in range(dir_count): + kept_for_direction = 0 + if audit_directions: + print(f"Direction {direction}:") + for input_index, (blocks, per_dir) in enumerate(zip(decoded_blocks, per_input_lengths), start=1): + if kept_for_direction >= kept_anim_length: + break + take = min(per_dir, kept_anim_length - kept_for_direction) + combined.extend(blocks[direction][:take]) + if audit_directions: + out_a = kept_for_direction + out_b = kept_for_direction + take - 1 + print(f" output frames {out_a}-{out_b}: input {input_index} direction {direction} frames 0-{take - 1}") + kept_for_direction += take + + total_frames = dir_count * kept_anim_length + total_anim_time_ms = combined_anim_time_ms( + decoded=decoded, + per_input_lengths=per_input_lengths, + kept_frames_per_direction=kept_anim_length, + fallback_speed_ms=max(1, first.speed_ms), + ) + + civ = Civ3Tail( + num_anims=dir_count, + anim_length=kept_anim_length, + x_offset=first.civ3.x_offset, + y_offset=first.civ3.y_offset, + xs_orig=first.civ3.xs_orig, + ys_orig=first.civ3.ys_orig, + anim_time_ms=total_anim_time_ms, + directions=first.civ3.directions, + ) + + return FlcDecoded( + w=first.w, + h=first.h, + file_frames=total_frames, + speed_ms=first.speed_ms, + creator=first.creator, + civ3=civ, + palette=first.palette, + anim_frames=combined, + include_ring_frame=False, + direction_uniques=[], + ) + + +def make_delta_flc_payload(prev: bytes, cur: bytes, w: int, h: int) -> bytes: + frame_dimensions(prev, w, h) + frame_dimensions(cur, w, h) + + changed_rows = [] + for y in range(h): + a = y * w + b = a + w + if prev[a:b] != cur[a:b]: + changed_rows.append(y) + + out = bytearray(pack_u16(len(changed_rows))) + cursor_y = 0 + for y in changed_rows: + skip = y - cursor_y + while skip > 0: + step = min(skip, 32767) + out.extend(struct.pack(" None: + if len(palette) != 768: + raise ValueError("Palette must be 768 bytes.") + if not frames: + raise ValueError("No frames to write.") + + w, h = decoded.w, decoded.h + for frame in frames: + frame_dimensions(frame, w, h) + + civ = decoded.civ3 + target_frames = max(1, decoded.file_frames) + if len(frames) != target_frames: + raise ValueError(f"Expected {target_frames} frames, got {len(frames)}.") + + dir_count = max(1, civ.num_anims) + anim_len = max(1, civ.anim_length) + if dir_count * anim_len != len(frames): + dir_count = 1 + anim_len = len(frames) + civ.num_anims = 1 + civ.anim_length = anim_len + + chunks: List[bytes] = [] + for d in range(dir_count): + base = d * anim_len + key_frame = frames[base] + key_subchunks = [make_subchunk(CHUNK_BYTE_RUN, make_byte_run_payload(key_frame, w, h))] + if d == 0: + key_subchunks.append(make_subchunk(CHUNK_COLOR_256, make_color_256_payload(bytes(palette)))) + chunks.append(make_frame_chunk(key_subchunks)) + + prev = key_frame + for i in range(anim_len): + cur = frames[base + i] + delta = make_delta_flc_payload(prev, cur, w, h) + chunks.append(make_frame_chunk([make_subchunk(CHUNK_DELTA_FLC, delta)])) + prev = cur + + if decoded.include_ring_frame and dir_count == 1: + delta = make_delta_flc_payload(frames[-1], frames[0], w, h) + chunks.append(make_frame_chunk([make_subchunk(CHUNK_DELTA_FLC, delta)])) + + body = b"".join(chunks) + header = build_flc_header_128( + total_file_size=128 + len(body), + frames_without_ring=target_frames, + w=w, + h=h, + speed_ms=decoded.speed_ms, + creator=decoded.creator, + oframe1=128, + oframe2=0, + civ3_tail_40=civ.pack_40_bytes(), + ) + + with open(path, "wb") as f: + f.write(header) + f.write(body) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Make a Civ3 FLC sprite look zoomed out while keeping the original FLC canvas and palette." + ) + parser.add_argument( + "--in", + dest="inputs", + nargs="+", + action="append", + required=True, + help="Input Civ3 FLC path(s). Repeat --in or pass multiple paths to concatenate animations.", + ) + parser.add_argument("--out", required=True, help="Output Civ3 FLC path.") + parser.add_argument( + "--scale", + type=float, + default=0.75, + help="Visible content scale, where 0.75 means 75%% of original size. Default: 0.75.", + ) + parser.add_argument( + "--background-index", + type=int, + default=None, + help="Palette index used to fill newly exposed border pixels. Default: auto-detect from frame edges.", + ) + parser.add_argument( + "--anchor", + type=parse_anchor, + default=(0.5, 0.5), + help="Anchor for scaling: center, bottom, top-left, etc., or x,y in 0..1. Default: center.", + ) + parser.add_argument( + "--per-frame-bbox", + action="store_true", + help="Scale each frame's own non-background bounding box instead of the shared animation bounds.", + ) + parser.add_argument("--shift-x", type=int, default=0, help="Move scaled content horizontally after scaling.") + parser.add_argument("--shift-y", type=int, default=0, help="Move scaled content vertically after scaling.") + parser.add_argument( + "--max-frames-per-direction", + type=int, + default=64, + help="Truncate each Civ3 direction to this many frames after concatenation. Use 0 for no cap. Default: 64.", + ) + parser.add_argument( + "--audit-directions", + action="store_true", + help="Print the per-direction input ranges used in the combined output.", + ) + + args = parser.parse_args() + + input_paths = flatten_inputs(args.inputs) + decoded_inputs = [decode_flc(path) for path in input_paths] + dec = combine_decoded_inputs( + decoded_inputs, + args.max_frames_per_direction, + audit_directions=args.audit_directions, + ) + bg = args.background_index + if bg is None: + bg = choose_background_index(dec.anim_frames, dec.w, dec.h) + if bg < 0 or bg > 255: + raise SystemExit("--background-index must be in the range 0..255.") + + scaled = scale_frames( + frames=dec.anim_frames, + w=dec.w, + h=dec.h, + scale=args.scale, + background_index=bg, + per_frame_bbox=args.per_frame_bbox, + anchor=args.anchor, + shift_x=args.shift_x, + shift_y=args.shift_y, + ) + + os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True) + write_zoom_flc(args.out, dec, scaled, dec.palette) + + print(f"Inputs: {len(input_paths)}") + for path in input_paths: + print(f" {path}") + print(f"Output: {args.out}") + print(f"Canvas: {dec.w}x{dec.h}") + print(f"Header animation frames: {dec.file_frames}") + print(f"Decoded/scaled animation frames: {len(scaled)}") + print(f"Palette source: first input ({len(dec.palette)} bytes)") + print(f"Background index: {bg}") + print(f"Scale: {args.scale:g}") + print(f"Civ3 directions: {dec.civ3.num_anims}") + print(f"Frames per direction: {dec.civ3.anim_length}") + print(f"Max frames per direction: {args.max_frames_per_direction or 'none'}") + print("Animation chunks: DELTA_FLC") + + +if __name__ == "__main__": + main() 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 30b81e19..d5957c9b 100644 --- a/civ_prog_objects.csv +++ b/civ_prog_objects.csv @@ -970,4 +970,9 @@ ignore, 0x4BF660, 0x4C6C10, 0x4BF6F0, "City_draw_citizens", "void (__fastc 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" - +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)" +inlead, 0x61B320, 0x0, 0x0, "Context_Menu_draw_item", "void (__fastcall *) (Context_Menu * this, int edx, int item_index, int redraw)" \ No newline at end of file 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 5364c9d9..a2675647 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -942,6 +942,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 ===] [=======================] diff --git a/default.districts_config.txt b/default.districts_config.txt index 17e9c5e2..e7ace01b 100644 --- a/default.districts_config.txt +++ b/default.districts_config.txt @@ -57,6 +57,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) @@ -295,6 +302,28 @@ 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..2568d3a0 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\AngelFalls.INI; hours=7-17; seasons=spring,summer,fall,winter; offsets=0,-10; direction=southwest; frame_time_seconds=0.13 +animation = ini=NaturalWonders\AngelFalls\AngelFalls_night.INI; hours=18-6; seasons=spring,summer,fall,winter; offsets=0,-10; direction=southwest; frame_time_seconds=0.13 #Wonder name = Yosemite @@ -81,6 +89,8 @@ food_bonus = 0 gold_bonus = 0 shield_bonus = 0 happiness_bonus = 1 +animation = ini=NaturalWonders\Yellowstone\Yellowstone.INI; hours=7-17; seasons=spring,summer,fall,winter; offsets=28,0; direction=southwest; frame_time_seconds=0.2 +animation = ini=NaturalWonders\Yellowstone\Yellowstone_night.INI; hours=18-6; seasons=spring,summer,fall,winter; offsets=28,0; direction=southwest; frame_time_seconds=0.2 #Wonder name = Mount Everest @@ -276,4 +286,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..4d845813 --- /dev/null +++ b/default.tile_animations.txt @@ -0,0 +1,214 @@ +[======================================================================= 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. + ; + ; Example PCX animation of a 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 + ; - 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 = Snow_night +ini_path = Terrain\Snow\Snow_night.INI +type = terrain +terrain_types = mountains, snow-mountains, volcanoes, snow-volcanoes +show_in_seasons = winter +show_in_day_night_hours = 18-6 + +#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\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\Cow\black_and_white_cow_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 = Elephant +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 = Elephant_night +ini_path = Resources\Elephant\Elephant_night.ini +type = resource +resource_type = Ivory +direction = southwest +frame_time_seconds = 0.17 +show_in_day_night_hours = 18-6 + +#Animation +name = Fish +ini_path = Resources\Fish\Fish.INI +type = resource +resource_type = Fish +frame_time_seconds = 0.11 +y_offset = -14 +x_offset = -2 + +#Animation +name = Furs +ini_path = Resources\Fox\Fox.ini +type = resource +resource_type = Furs +direction = southwest +show_in_day_night_hours = 7-17 + +#Animation +name = Furs_night +ini_path = Resources\Fox\Fox_night.ini +type = resource +resource_type = Furs +direction = southwest +show_in_day_night_hours = 18-6 + +#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 \ No newline at end of file diff --git a/injected_code.c b/injected_code.c index 5b81a7ab..81be01b1 100644 --- a/injected_code.c +++ b/injected_code.c @@ -92,8 +92,6 @@ struct injected_state * is = ADDR_INJECTED_STATE; // used to limit computational complexity #define AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES 10 -enum { NAMED_TILE_MENU_ID = 0x90 }; - char const * const hotseat_replay_save_path = "Saves\\Auto\\ai-move-replay-before-interturn.SAV"; char const * const hotseat_resume_save_path = "Saves\\Auto\\ai-move-replay-resume.SAV"; @@ -229,6 +227,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); @@ -246,12 +245,46 @@ bool tile_coords_has_city_with_building_in_district_radius (int tile_x, int tile void __fastcall patch_Trade_Net_recompute_resources (Trade_Net * this, int edx, bool skip_popups); int get_visible_non_subsumed_tile_resource (Tile * tile, struct district_instance * inst, int civ_id); void recompute_distribution_hub_totals (); +void init_distribution_hub_icons (); void get_neighbor_coords (Map * map, int x, int y, int neighbor_index, int * out_x, int * out_y); void wrap_tile_coords (Map * map, int * x, int * y); 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 (); +bool is_custom_tile_animation_effect (int effect_id); +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 @@ -2060,7 +2093,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}, @@ -2070,7 +2103,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}, @@ -2366,6 +2399,12 @@ read_barbarian_activity_override (struct string_slice const * s, enum barbarian_ return found; } +bool +read_tile_animation_direction_value (struct string_slice const * s, enum direction * out_dir) +{ + return read_direction_value (s, out_dir); +} + int read_units_per_tile_limit (struct string_slice const * s, int * out_limits) { @@ -2620,6 +2659,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}, @@ -3104,8 +3153,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); } } @@ -6633,7 +6686,10 @@ district_is_complete(Tile * tile, int district_id) } } if (worker_to_consume != NULL) { - patch_Unit_despawn (worker_to_consume, __, 0, true, false, 0, 0, 0, 0); + if ((p_main_screen_form != NULL) && + patch_Leader_is_tile_visible (&leaders[p_main_screen_form->Player_CivID], __, worker_to_consume->Body.X, worker_to_consume->Body.Y)) { + patch_Unit_despawn (worker_to_consume, __, 0, true, false, 0, 0, 0, 0); + } } } @@ -6669,6 +6725,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); if (cfg->subsumes_tile_resource && is->trade_net != NULL) patch_Trade_Net_recompute_resources (is->trade_net, __, false); @@ -6857,6 +6914,198 @@ distribution_hub_accessible_to_city (struct distribution_hub_record * rec, City return Trade_Net_have_trade_connection (is->trade_net, __, anchor_city, city, rec->civ_id); } +bool +distribution_hub_city_is_selected (struct distribution_hub_record * rec, City * city) +{ + if ((rec == NULL) || (city == NULL)) + return false; + + return itable_look_up_or (&rec->selected_city_ids, city->Body.ID, 0) != 0; +} + +bool +distribution_hub_distributes_to_city (struct distribution_hub_record * rec, City * city) +{ + if (! distribution_hub_accessible_to_city (rec, city)) + return false; + + if ((is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT) && + (rec->city_selection_mode == DHCSM_SPECIFIC_CITIES)) + return distribution_hub_city_is_selected (rec, city); + + return true; +} + +void +clear_distribution_hub_city_selection (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return; + + table_deinit (&rec->selected_city_ids); +} + +void +select_all_accessible_distribution_hub_cities (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return; + + clear_distribution_hub_city_selection (rec); + FOR_CITIES_OF (coi, rec->civ_id) { + City * city = coi.city; + if ((city != NULL) && distribution_hub_accessible_to_city (rec, city)) + itable_insert (&rec->selected_city_ids, city->Body.ID, 1); + } +} + +int +count_distribution_hub_target_cities (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return 0; + + int count = 0; + FOR_CITIES_OF (coi, rec->civ_id) { + City * city = coi.city; + if ((city != NULL) && distribution_hub_distributes_to_city (rec, city)) + count++; + } + return count; +} + +void +recompute_distribution_hub_cities_for_civ (int civ_id) +{ + if ((civ_id < 0) || (civ_id >= 32) || (p_cities->Cities == NULL)) + return; + + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && (city->Body.CivID == civ_id)) + recompute_city_yields_with_districts (city); + } +} + +int +ai_score_distribution_hub_target_city (City * city) +{ + if (city == NULL) + return 0; + + int score = 0; + + int net_food = city->Body.FoodIncome; + if (net_food <= 0) + score += 1000; + else if (net_food <= 2) + score += 500; + + int net_shields = city->Body.ProductionIncome + city->Body.ProductionLoss; + if (net_shields < 0) + net_shields = 0; + if (net_shields <= 3) + score += 700; + else if (net_shields <= 6) + score += 350; + + return score; +} + +bool +ai_update_distribution_hub_city_selection (struct distribution_hub_record * rec) +{ + if ((rec == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return false; + + int civ_id = rec->civ_id; + if ((civ_id < 0) || (civ_id >= 32)) + return false; + if ((1u << civ_id) & *p_human_player_bits) + return false; + + struct table selected = {0}; + int accessible_count = 0; + int selected_count = 0; + + FOR_CITIES_OF (coi, civ_id) { + City * city = coi.city; + if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) + continue; + + accessible_count++; + if (ai_score_distribution_hub_target_city (city) > 0) { + itable_insert (&selected, city->Body.ID, 1); + selected_count++; + } + } + + if ((accessible_count <= 1) || + (selected_count <= 0) || + (selected_count >= accessible_count)) { + table_deinit (&selected); + if ((rec->city_selection_mode == DHCSM_ALL_CITIES) && (rec->selected_city_ids.len == 0)) + return false; + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + return true; + } + + bool changed = rec->city_selection_mode != DHCSM_SPECIFIC_CITIES; + if (rec->selected_city_ids.len != selected.len) + changed = true; + else { + FOR_TABLE_ENTRIES (tei, &selected) { + if (! itable_look_up_or (&rec->selected_city_ids, tei.key, 0)) { + changed = true; + break; + } + } + } + + if (! changed) { + table_deinit (&selected); + return false; + } + + clear_distribution_hub_city_selection (rec); + rec->selected_city_ids = selected; + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + return true; +} + +void +ai_update_distribution_hub_city_selections_for_leader (Leader * leader) +{ + if ((leader == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return; + + int civ_id = leader->ID; + if ((civ_id < 0) || (civ_id >= 32)) + return; + if ((1u << civ_id) & *p_human_player_bits) + return; + + bool changed = false; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if ((rec != NULL) && (rec->civ_id == civ_id) && ai_update_distribution_hub_city_selection (rec)) + changed = true; + } + + if (changed) { + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (civ_id); + } +} + void get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) { @@ -6872,7 +7121,7 @@ get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shi FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; - if (distribution_hub_accessible_to_city (rec, city)) { + if (distribution_hub_distributes_to_city (rec, city)) { food += rec->food_yield; shields += rec->shield_yield; } @@ -6953,6 +7202,7 @@ clear_distribution_hub_tables (void) { FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + clear_distribution_hub_city_selection (rec); free (rec); } table_deinit (&is->distribution_hub_records); @@ -7073,18 +7323,14 @@ recompute_distribution_hub_yields (struct distribution_hub_record * rec) if (shield_div <= 0) shield_div = 1; - int connected_city_count = 0; - if (anchor_city != NULL) { - FOR_CITIES_OF (coi, rec->civ_id) { - City * other_city = coi.city; - if ((other_city != NULL) && distribution_hub_accessible_to_city (rec, other_city)) - connected_city_count++; - } - } - if (connected_city_count <= 0) - connected_city_count = 1; + int connected_city_count = count_distribution_hub_target_cities (rec); if (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT) { + if (connected_city_count <= 0) { + rec->food_yield = 0; + rec->shield_yield = 0; + return; + } int city_root = 1; while ((city_root + 1) * (city_root + 1) <= connected_city_count) city_root++; @@ -7098,6 +7344,11 @@ recompute_distribution_hub_yields (struct distribution_hub_record * rec) rec->food_yield = food_sum / food_div; rec->shield_yield = shield_sum / shield_div; } + + if ((food_sum > 0) && (rec->food_yield <= 0)) + rec->food_yield = 1; + if ((shield_sum > 0) && (rec->shield_yield <= 0)) + rec->shield_yield = 1; } void @@ -7110,18 +7361,12 @@ remove_distribution_hub_record (Tile * tile) int affected_civ_id = rec->civ_id; release_distribution_hub_coverage (rec); itable_remove (&is->distribution_hub_records, (int)tile); + clear_distribution_hub_city_selection (rec); free (rec); is->distribution_hub_totals_dirty = true; recompute_distribution_hub_totals (); - // Recalculate yields for all cities of this civ - if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) - recompute_city_yields_with_districts (target_city); - } - } + recompute_distribution_hub_cities_for_civ (affected_civ_id); } void @@ -7162,9 +7407,14 @@ recompute_distribution_hub_totals () int old_civ_id = rec->civ_id; rec->civ_id = current_tile->vtable->m38_Get_Territory_OwnerID (current_tile); - if (old_civ_id != rec->civ_id) + if ((old_civ_id != rec->civ_id) && (old_civ_id >= 0) && (old_civ_id < 32)) civs_needing_recalc[old_civ_id] = 1; - civs_needing_recalc[rec->civ_id] = 1; + if ((rec->civ_id >= 0) && (rec->civ_id < 32)) + civs_needing_recalc[rec->civ_id] = 1; + if (old_civ_id != rec->civ_id) { + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + } City * anchor = get_connected_city_for_distribution_hub (rec); @@ -7245,13 +7495,8 @@ recompute_distribution_hub_totals () // Recalculate yields for cities of civs whose distribution hub ownership changed for (int civ_id = 0; civ_id < 32; civ_id++) { - if (civs_needing_recalc[civ_id] && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && (city->Body.CivID == civ_id)) - recompute_city_yields_with_districts (city); - } - } + if (civs_needing_recalc[civ_id]) + recompute_distribution_hub_cities_for_civ (civ_id); } is->distribution_hub_totals_dirty = false; @@ -7276,17 +7521,16 @@ on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) release_distribution_hub_coverage (rec); rec->civ_id = tile_owner; + if (old_civ_id != tile_owner) { + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + } is->distribution_hub_totals_dirty = true; recompute_distribution_hub_totals (); - if (old_civ_id != tile_owner) { - // Recompute for old civ - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == old_civ_id)) - recompute_city_yields_with_districts (target_city); - } - } + recompute_distribution_hub_cities_for_civ (old_civ_id); + recompute_distribution_hub_cities_for_civ (tile_owner); + return; } rec = malloc (sizeof *rec); @@ -7296,6 +7540,8 @@ on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) rec->tile_x = tile_x; rec->tile_y = tile_y; rec->civ_id = tile_owner; + rec->city_selection_mode = DHCSM_ALL_CITIES; + memset (&rec->selected_city_ids, 0, sizeof rec->selected_city_ids); rec->food_yield = 0; rec->shield_yield = 0; rec->raw_food_yield = 0; @@ -7306,15 +7552,8 @@ on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) is->distribution_hub_totals_dirty = true; recompute_distribution_hub_totals (); - // Recalculate yields for all cities of this civ int affected_civ_id = rec->civ_id; - if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) - recompute_city_yields_with_districts (target_city); - } - } + recompute_distribution_hub_cities_for_civ (affected_civ_id); } void @@ -7479,6 +7718,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) { @@ -7604,6 +7878,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); } @@ -7669,6 +7944,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; @@ -7826,6 +8102,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 @@ -8039,6 +8316,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); } @@ -8864,6 +9142,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) { @@ -9168,6 +9452,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; @@ -9854,6 +10142,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; @@ -10998,6 +11297,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; @@ -11046,6 +11353,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; @@ -11116,6 +11451,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, @@ -11308,6 +11771,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); } @@ -13874,7 +14348,7 @@ find_tile_for_neighborhood_district (City * city, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_distribution_hub_districts) { + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); if (covered > 0) continue; @@ -13942,7 +14416,7 @@ find_tile_for_port_district (City * city, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_distribution_hub_districts) { + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); if (covered > 0) continue; @@ -14534,7 +15008,7 @@ find_tile_for_district (City * city, int district_id, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_distribution_hub_districts) { + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); if (covered > 0) continue; @@ -15378,6 +15852,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); } @@ -17320,7 +17795,7 @@ join_path(char *out, size_t out_sz, const char *dir, const char *file) snprintf(out, out_sz, "%s%s%s", dir, need_sep ? "\\" : "", file); } -void +void read_in_dir(PCX_Image *img, const char *art_dir, const char *filename, @@ -17335,9 +17810,30 @@ read_in_dir(PCX_Image *img, char temp_path[2*MAX_PATH]; snprintf(temp_path, sizeof temp_path, "%s\\%s", art_dir, filename); + if (img->JGL.Image != NULL) + img->vtable->clear_JGL (img); PCX_Image_read_file(img, __, temp_path, NULL, 0, 0x100, 2); } +bool +construct_cycle_sprite_array (Sprite ** out_sprites, int count) +{ + *out_sprites = NULL; + if (count <= 0) + return true; + + Sprite * sprites = malloc (count * sizeof sprites[0]); + if (sprites == NULL) + return false; + + memset (sprites, 0, count * sizeof sprites[0]); + for (int i = 0; i < count; i++) + Sprite_construct (&sprites[i]); + + *out_sprites = sprites; + return true; +} + 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]; @@ -17572,15 +18068,22 @@ bool load_day_night_hour_and_season_images(struct day_night_cycle_img_set *this, if (img.JGL.Image == NULL) return false; Sprite_slice_pcx(&this->Victory_Image, __, &img, 0, 0, 0x80, 0x40, 1, 1); - // Resources - read_in_dir(&img, art_dir, "resources.pcx", NULL); + // Resources + read_in_dir(&img, art_dir, "resources.pcx", NULL); if (img.JGL.Image == NULL) return false; - size_t k = 0; - for (int r = 0, y = 1; r < 6; ++r, y += 50) { - for (int c = 0, x = 1; c < 6; ++c, x += 50) { - Sprite_slice_pcx(&this->Resources[k++], __, &img, x, y, 49, 49, 1, 1); - } - } + int resource_cols = img.JGL.Image->vtable->m54_Get_Width (img.JGL.Image) / 50; + int resource_rows = img.JGL.Image->vtable->m55_Get_Height (img.JGL.Image) / 50; + int resource_count = resource_cols * resource_rows; + if ((resource_cols <= 0) || (resource_rows <= 0) || + ! construct_cycle_sprite_array (&this->Resources, resource_count)) + return false; + this->ResourceCount = resource_count; + int k = 0; + for (int r = 0, y = 1; r < resource_rows; ++r, y += 50) { + for (int c = 0, x = 1; c < resource_cols; ++c, x += 50) { + Sprite_slice_pcx(&this->Resources[k++], __, &img, x, y, 49, 49, 1, 1); + } + } // Base cities static const char *CITY_BASE[5] = { @@ -17687,6 +18190,8 @@ bool load_day_night_hour_and_season_images(struct day_night_cycle_img_set *this, char const * img_path = cfg->img_path; if (img_path == NULL) img_path = "Wonders.pcx"; + int sprite_width = (cfg->custom_width > 0) ? cfg->custom_width : 128; + int sprite_height = (cfg->custom_height > 0) ? cfg->custom_height : 64; // Load new image file if different from previous if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { @@ -17710,25 +18215,25 @@ bool load_day_night_hour_and_season_images(struct day_night_cycle_img_set *this, struct wonder_district_image_set * set = &this->Wonder_District_Images[wi]; Sprite_construct (&set->img); - int x = 128 * cfg->img_column; - int y = 64 * cfg->img_row; - Sprite_slice_pcx (&set->img, __, &wpcx, x, y, 128, 64, 1, 1); + int x = sprite_width * cfg->img_column; + int y = sprite_height * cfg->img_row; + Sprite_slice_pcx (&set->img, __, &wpcx, x, y, sprite_width, sprite_height, 1, 1); Sprite_construct (&set->construct_img); - int cx = 128 * cfg->img_construct_column; - int cy = 64 * cfg->img_construct_row; - Sprite_slice_pcx (&set->construct_img, __, &wpcx, cx, cy, 128, 64, 1, 1); + int cx = sprite_width * cfg->img_construct_column; + int cy = sprite_height * cfg->img_construct_row; + Sprite_slice_pcx (&set->construct_img, __, &wpcx, cx, cy, sprite_width, sprite_height, 1, 1); if (cfg->enable_img_alt_dir) { Sprite_construct (&set->alt_dir_img); - int ax = 128 * cfg->img_alt_dir_column; - int ay = 64 * cfg->img_alt_dir_row; - Sprite_slice_pcx (&set->alt_dir_img, __, &wpcx, ax, ay, 128, 64, 1, 1); + int ax = sprite_width * cfg->img_alt_dir_column; + int ay = sprite_height * cfg->img_alt_dir_row; + Sprite_slice_pcx (&set->alt_dir_img, __, &wpcx, ax, ay, sprite_width, sprite_height, 1, 1); Sprite_construct (&set->alt_dir_construct_img); - int acx = 128 * cfg->img_alt_dir_construct_column; - int acy = 64 * cfg->img_alt_dir_construct_row; - Sprite_slice_pcx (&set->alt_dir_construct_img, __, &wpcx, acx, acy, 128, 64, 1, 1); + int acx = sprite_width * cfg->img_alt_dir_construct_column; + int acy = sprite_height * cfg->img_alt_dir_construct_row; + Sprite_slice_pcx (&set->alt_dir_construct_img, __, &wpcx, acx, acy, sprite_width, sprite_height, 1, 1); } } @@ -17797,11 +18302,8 @@ get_cycle_sprite_proxy(Sprite *s) { 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)) + if (itable_look_up (is->day_night_sprite_proxy_by_season_and_hour, (int)s, &v)) return (Sprite *)v; return NULL; } @@ -17810,13 +18312,12 @@ void 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_season_and_hour[cycle_idx], (int)s, (int)p); + itable_insert(is->day_night_sprite_proxy_by_season_and_hour, (int)s, (int)p); } } } @@ -17826,12 +18327,11 @@ void 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_season_and_hour[cycle_idx], (int)s, (int)p); + itable_insert(is->day_night_sprite_proxy_by_season_and_hour, (int)s, (int)p); } } } @@ -17840,29 +18340,26 @@ void 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_season_and_hour[cycle_idx], (int)s, (int)p); + itable_insert(is->day_night_sprite_proxy_by_season_and_hour, (int)s, (int)p); } } 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]); + is->cycle_imgs = malloc (sizeof is->cycle_imgs[0]); if (is->cycle_imgs == NULL) return false; - memset (is->cycle_imgs, 0, count * sizeof is->cycle_imgs[0]); + memset (is->cycle_imgs, 0, 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]); + is->day_night_sprite_proxy_by_season_and_hour = malloc (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]); + memset (is->day_night_sprite_proxy_by_season_and_hour, 0, sizeof is->day_night_sprite_proxy_by_season_and_hour[0]); } return true; @@ -17899,36 +18396,6 @@ get_next_enabled_season (int current_season, int mask) 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; - - 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 () { @@ -17945,131 +18412,182 @@ get_current_local_season () 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) +char const * +get_day_night_cycle_hour_str (int hour) +{ + char const * hour_strs[24] = { + "2400", "0100", "0200", "0300", "0400", "0500", "0600", "0700", + "0800", "0900", "1000", "1100", "1200", "1300", "1400", "1500", + "1600", "1700", "1800", "1900", "2000", "2100", "2200", "2300" + }; + return hour_strs[clamp (0, 23, hour)]; +} + +void +deinit_sprite_if_needed (Sprite * sprite) +{ + if (sprite->vtable != NULL) + sprite->vtable->destruct (sprite, __, 0); +} + +void +deinit_cycle_sprite_array (Sprite ** sprites, int count) +{ + if (*sprites == 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; - if (variant_count > variant_capacity) - variant_count = variant_capacity; + for (int i = 0; i < count; i++) + deinit_sprite_if_needed (&(*sprites)[i]); + free (*sprites); + *sprites = NULL; +} - int era_count = cfg->vary_img_by_era ? 4 : 1; - int column_count = cfg->img_column_count; +void +construct_day_night_cycle_img_set (struct day_night_cycle_img_set * set) +{ + if (set == NULL) + return; - 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); - } - } - } - } + Sprite * sprites = (Sprite *)set; + int sprite_count = (int)(((char *)&set->Resources - (char *)set) / sizeof sprites[0]); + for (int i = 0; i < sprite_count; i++) + Sprite_construct (&sprites[i]); +} + +void +deinit_day_night_cycle_img_set (struct day_night_cycle_img_set * set) +{ + if (set == NULL) + return; - 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); + Sprite * sprites = (Sprite *)set; + int sprite_count = (int)(((char *)&set->Resources - (char *)set) / sizeof sprites[0]); + for (int i = 0; i < sprite_count; i++) + deinit_sprite_if_needed (&sprites[i]); + deinit_cycle_sprite_array (&set->Resources, set->ResourceCount); + memset (set, 0, sizeof *set); +} + +void +build_sprite_proxies(Map_Renderer *mr) { + if (is->cycle_imgs == NULL || is->day_night_sprite_proxy_by_season_and_hour == NULL) + return; - 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); + int season = (is->current_config.seasonal_cycle_mode != SCM_OFF) ? clamp (0, 3, is->current_seasonal_cycle) : CS_SUMMER; + int h = (is->current_config.day_night_cycle_mode != DNCM_OFF) ? clamp (0, 23, is->current_day_night_cycle) : 12; + struct day_night_cycle_img_set * set = is->cycle_imgs; + + insert_sprite_proxies(city_sprites, set->City_Images, season, h, 80); + insert_sprite_proxies(destroyed_city_sprites, set->Destroyed_City_Images, season, h, 3); + if ((mr->Resources != NULL) && (set->Resources != NULL)) { + int resource_count = ((int *)mr->Resources)[-1]; + if (resource_count > set->ResourceCount) + resource_count = set->ResourceCount; + insert_sprite_proxies(mr->Resources, set->Resources, season, h, resource_count); + } + 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->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); + 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; + 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, &set->Abandoned_District_Image, season, h); + insert_sprite_proxy (&is->abandoned_maritime_district_img, &set->Abandoned_Maritime_District_Image, season, 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); + 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_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); } } } + + 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); + } is->day_night_cycle_img_proxies_indexed = true; } - void init_day_night_and_seasonal_images() { @@ -18078,36 +18596,25 @@ init_day_night_and_seasonal_images() if (is->cycle_imgs == NULL) return; - const char *hour_strs[24] = { - "2400", "0100", "0200", "0300", "0400", "0500", "0600", "0700", - "0800", "0900", "1000", "1100", "1200", "1300", "1400", "1500", - "1600", "1700", "1800", "1900", "2000", "2100", "2200", "2300" - }; - - 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; + 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; + char const * hour_str = get_day_night_cycle_hour_str (hour); - 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]); + char art_dir[200]; + char temp_path[2*MAX_PATH]; + snprintf (art_dir, sizeof art_dir, "DayNight/%s/%s", cycle_season_names[season], hour_str); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); + construct_day_night_cycle_img_set (is->cycle_imgs); + bool success = load_day_night_hour_and_season_images (is->cycle_imgs, temp_path, cycle_season_names[season], hour_str); - 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); + 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_str); + pop_up_in_game_error (ss); - is->day_night_cycle_img_state = IS_INIT_FAILED; - return; - } - } + deinit_day_night_cycle_img_set (is->cycle_imgs); + is->day_night_cycle_img_state = IS_INIT_FAILED; + return; } Map_Renderer * mr = &p_bic_data->Map.Renderer; @@ -18122,12 +18629,31 @@ deindex_day_night_image_proxies() if (!is->day_night_cycle_img_proxies_indexed || is->day_night_sprite_proxy_by_season_and_hour == NULL) return; - 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]); + table_deinit (is->day_night_sprite_proxy_by_season_and_hour); is->day_night_cycle_img_proxies_indexed = false; } +bool +reload_current_day_night_and_seasonal_images (Map_Renderer * mr) +{ + if (! allocate_day_night_cycle_runtime_storage ()) { + is->day_night_cycle_img_state = IS_INIT_FAILED; + return false; + } + + if (is->day_night_cycle_img_proxies_indexed) + deindex_day_night_image_proxies (); + + deinit_day_night_cycle_img_set (is->cycle_imgs); + is->day_night_cycle_img_state = IS_UNINITED; + init_day_night_and_seasonal_images (); + + if ((is->day_night_cycle_img_state == IS_OK) && ! is->day_night_cycle_img_proxies_indexed) + build_sprite_proxies (mr); + + return is->day_night_cycle_img_state == IS_OK; +} + int calculate_current_seasonal_cycle (bool transition_on_day_night_hour_hit) { @@ -18479,6 +19005,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)}, @@ -18541,13 +19068,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)}, @@ -18640,6 +19168,7 @@ patch_init_floating_point () 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++) @@ -18847,6 +19376,16 @@ patch_init_floating_point () 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_selected_hour = -1; + is->tile_animation_selected_season = -1; + 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; @@ -19307,16 +19846,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); + } } } @@ -19331,6 +19874,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 @@ -22031,6 +22581,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; @@ -22058,6 +22609,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)) { @@ -22295,6 +22847,9 @@ patch_load_scenario (BIC * this, int edx, char * param_1, unsigned * param_2) is->turns_in_current_season = 0; if (is->day_night_cycle_img_proxies_indexed) deindex_day_night_image_proxies (); + if (is->cycle_imgs != NULL) + deinit_day_night_cycle_img_set (is->cycle_imgs); + is->day_night_cycle_img_state = IS_UNINITED; 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 ()) { @@ -22306,11 +22861,10 @@ patch_load_scenario (BIC * this, int edx, char * param_1, unsigned * param_2) 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)) { + if (is->cycle_imgs != NULL) { free (is->cycle_imgs); is->cycle_imgs = NULL; } - is->day_night_cycle_img_state = IS_UNINITED; } return tr; @@ -24005,6 +24559,145 @@ patch_Context_Menu_add_item_and_set_color (Context_Menu * this, int edx, int ite return tr; } +bool +distribution_hub_city_select_ui_enabled (void) +{ + return is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts && + (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT); +} + +struct distribution_hub_record * +get_active_distribution_hub_menu_record (void) +{ + if (! is->distribution_hub_menu_active) + return NULL; + + if (! distribution_hub_city_select_ui_enabled ()) + return NULL; + + Tile * tile = tile_at (is->distribution_hub_menu_tile_x, is->distribution_hub_menu_tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return NULL; + + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || + (inst->district_id != DISTRIBUTION_HUB_DISTRICT_ID) || + ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID)) + return NULL; + + if (is->distribution_hub_totals_dirty && + ! is->distribution_hub_refresh_in_progress) + recompute_distribution_hub_totals (); + + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec == NULL) { + on_distribution_hub_completed (tile, is->distribution_hub_menu_tile_x, is->distribution_hub_menu_tile_y); + rec = get_distribution_hub_record (tile); + } + if ((rec == NULL) || (rec->civ_id != p_main_screen_form->Player_CivID)) + return NULL; + + return rec; +} + +bool +distribution_hub_menu_can_open_on_tile (Tile * tile, int tile_x, int tile_y) +{ + if (! distribution_hub_city_select_ui_enabled () || + (tile == NULL) || + (tile == p_null_tile)) + return false; + + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || + (inst->district_id != DISTRIBUTION_HUB_DISTRICT_ID) || + ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID)) + return false; + + int owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + return owner == p_main_screen_form->Player_CivID; +} + +void +add_distribution_hub_menu_items (Context_Menu * menu, struct distribution_hub_record * rec) +{ + if ((menu == NULL) || (rec == NULL)) + return; + + if (menu->Item_Count > 0) + Context_Menu_add_separator (menu, __, 0); + + bool specific = rec->city_selection_mode == DHCSM_SPECIFIC_CITIES; + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID, "Distribute to All Cities", false, (Sprite *)0x0); + if (specific) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID); + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID, "Distribute to Specific Cities", false, (Sprite *)0x0); + if (! specific) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID); + Context_Menu_add_separator (menu, __, 0); + + FOR_CITIES_OF (coi, rec->civ_id) { + City * city = coi.city; + if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) + continue; + + char menu_text[160]; + snprintf (menu_text, sizeof menu_text, "%s", city->Body.CityName); + menu_text[sizeof menu_text - 1] = '\0'; + + Sprite * icon_sentinel = NULL; + if (distribution_hub_distributes_to_city (rec, city) && + ((rec->food_yield > 0) || (rec->shield_yield > 0))) + icon_sentinel = &is->distribution_hub_menu_icon_sentinel; + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_CITY_ID_BASE + city->Body.ID, menu_text, false, icon_sentinel); + if (specific && ! distribution_hub_city_is_selected (rec, city)) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_CITY_ID_BASE + city->Body.ID); + } +} + +bool +handle_distribution_hub_menu_selection (int item_id) +{ + struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); + if (rec == NULL) + return false; + + int affected_civ_id = rec->civ_id; + + if (item_id == DISTRIBUTION_HUB_MENU_ALL_ID) { + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + } else if (item_id == DISTRIBUTION_HUB_MENU_SPECIFIC_ID) { + if (rec->city_selection_mode != DHCSM_SPECIFIC_CITIES) + select_all_accessible_distribution_hub_cities (rec); + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + } else if (item_id >= DISTRIBUTION_HUB_MENU_CITY_ID_BASE) { + int city_id = item_id - DISTRIBUTION_HUB_MENU_CITY_ID_BASE; + City * city = get_city_ptr (city_id); + if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) + return false; + + if (rec->city_selection_mode != DHCSM_SPECIFIC_CITIES) { + select_all_accessible_distribution_hub_cities (rec); + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + } + + if (distribution_hub_city_is_selected (rec, city)) + itable_remove (&rec->selected_city_ids, city->Body.ID); + else + itable_insert (&rec->selected_city_ids, city->Body.ID, 1); + } else + return false; + + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (affected_civ_id); + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + is->distribution_hub_menu_reopen_requested = true; + return true; +} + int __fastcall patch_Context_Menu_open (Context_Menu * this, int edx, int x, int y, int param_3) { @@ -24029,6 +24722,10 @@ patch_Context_Menu_open (Context_Menu * this, int edx, int x, int y, int param_3 } } + struct distribution_hub_record * hub_rec = get_active_distribution_hub_menu_record (); + if (hub_rec != NULL) + add_distribution_hub_menu_items (this, hub_rec); + if (is->current_config.group_units_on_right_click_menu && (ret_addr == ADDR_OPEN_UNIT_MENU_RETURN) && (is->unit_menu_duplicates != NULL)) { @@ -24485,9 +25182,96 @@ patch_City_compute_corrupted_yield (City * this, int edx, int gross_yield, bool return tr; } +void +draw_distribution_hub_menu_icons (PCX_Image * canvas, int image_x, int row_center_y) +{ + if ((canvas == NULL) || + (canvas->JGL.Image == NULL) || + ! is->distribution_hub_menu_active || + ! distribution_hub_city_select_ui_enabled ()) + return; + + struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); + if (rec == NULL) + return; + + int food_yield = rec->food_yield; + int shield_yield = rec->shield_yield; + int total_yield = food_yield + shield_yield; + if (total_yield <= 0) + return; + + if (is->distribution_hub_icons_img_state == IS_UNINITED) + init_distribution_hub_icons (); + if (is->distribution_hub_icons_img_state != IS_OK) + return; + + Sprite * food_sprite = &is->distribution_hub_food_icon_small; + Sprite * shield_sprite = &is->distribution_hub_shield_icon_small; + + if (food_sprite->Width3 == 0) food_sprite = &is->distribution_hub_food_icon; + if (shield_sprite->Width3 == 0) shield_sprite = &is->distribution_hub_shield_icon; + + int food_width = food_sprite->Width3; + int shield_width = shield_sprite->Width3; + if ((food_width <= 0) && (shield_width > 0)) food_width = shield_width; + if ((shield_width <= 0) && (food_width > 0)) shield_width = food_width; + + int sprite_width = food_width > shield_width ? food_width : shield_width; + int sprite_height = food_sprite->Height; + if (sprite_height == 0) sprite_height = shield_sprite->Height; + if ((sprite_width <= 0) || (sprite_height <= 0)) + return; + + RECT menu_rect = canvas->JGL.Image->Image_Rect; + int menu_width = menu_rect.right - menu_rect.left; + if (menu_width <= 0) + return; + + int icon_left = menu_rect.left + (menu_width >> 1); + int icon_right_edge = menu_rect.right - 6; + int icon_band_width = icon_right_edge - icon_left; + if (icon_band_width < sprite_width) + return; + + int spacing = sprite_width; + if ((total_yield > 1) && (sprite_width * total_yield > icon_band_width)) { + spacing = (icon_band_width - sprite_width) / (total_yield - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > sprite_width) + spacing = sprite_width; + } + + int used_width = sprite_width + spacing * (total_yield - 1); + int pixel_x = icon_right_edge - used_width; + if (pixel_x < icon_left) + pixel_x = icon_left; + + int pixel_y = row_center_y - (sprite_height >> 1); + if (pixel_y < menu_rect.top) + pixel_y = menu_rect.top; + + for (int i = 0; i < food_yield; i++) { + Sprite_draw (food_sprite, __, canvas, pixel_x + ((sprite_width - food_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < shield_yield; i++) { + Sprite_draw (shield_sprite, __, canvas, pixel_x + ((sprite_width - shield_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } +} + int __fastcall patch_Sprite_draw (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { + if (this == &is->distribution_hub_menu_icon_sentinel) { + if (is->distribution_hub_menu_active && + distribution_hub_city_select_ui_enabled ()) + draw_distribution_hub_menu_icons (canvas, pixel_x, pixel_y); + return 0; + } + Sprite * to_draw = get_cycle_sprite_proxy(this); return Sprite_draw(to_draw ? to_draw : this, __, canvas, pixel_x, pixel_y, color_table); } @@ -24495,6 +25279,8 @@ patch_Sprite_draw (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int 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) { + 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); } @@ -25713,6 +26499,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); } @@ -25790,16 +26586,26 @@ handle_named_tile_menu_selection (void) void __fastcall patch_Main_Screen_Form_handle_right_click_on_tile (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y) { - if (is->current_config.enable_named_tiles) { - Tile * tile = tile_at (tile_x, tile_y); - if (tile_can_be_named (tile, tile_x, tile_y) && ! Tile_has_city (tile)) { - is->named_tile_menu_active = true; - is->named_tile_menu_tile_x = tile_x; - is->named_tile_menu_tile_y = tile_y; + Tile * tile = tile_at (tile_x, tile_y); + bool named_tile_active = is->current_config.enable_named_tiles && tile_can_be_named (tile, tile_x, tile_y) && ! Tile_has_city (tile); + bool hub_menu_active = distribution_hub_menu_can_open_on_tile (tile, tile_x, tile_y); + + if (named_tile_active || hub_menu_active) { + is->named_tile_menu_active = named_tile_active; + is->named_tile_menu_tile_x = tile_x; + is->named_tile_menu_tile_y = tile_y; + is->distribution_hub_menu_active = hub_menu_active; + is->distribution_hub_menu_tile_x = tile_x; + is->distribution_hub_menu_tile_y = tile_y; + do { + is->distribution_hub_menu_reopen_requested = false; Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); - is->named_tile_menu_active = false; - return; - } + } while (is->distribution_hub_menu_reopen_requested && + distribution_hub_menu_can_open_on_tile (tile_at (tile_x, tile_y), tile_x, tile_y)); + is->named_tile_menu_active = false; + is->distribution_hub_menu_active = false; + is->distribution_hub_menu_reopen_requested = false; + return; } Main_Screen_Form_handle_right_click_on_tile (this, __, tile_x, tile_y, mouse_x, mouse_y); @@ -25809,18 +26615,30 @@ void __fastcall patch_Main_Screen_Form_open_right_click_menu (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y) { bool set_active = false; - if (!is->named_tile_menu_active && is->current_config.enable_named_tiles) { + if (! is->named_tile_menu_active && ! is->distribution_hub_menu_active) { Tile * tile = tile_at (tile_x, tile_y); - if (tile_can_be_named (tile, tile_x, tile_y)) { - is->named_tile_menu_active = true; + bool named_tile_active = is->current_config.enable_named_tiles && tile_can_be_named (tile, tile_x, tile_y); + bool hub_menu_active = distribution_hub_menu_can_open_on_tile (tile, tile_x, tile_y); + if (named_tile_active || hub_menu_active) { + is->named_tile_menu_active = named_tile_active; is->named_tile_menu_tile_x = tile_x; is->named_tile_menu_tile_y = tile_y; + is->distribution_hub_menu_active = hub_menu_active; + is->distribution_hub_menu_tile_x = tile_x; + is->distribution_hub_menu_tile_y = tile_y; set_active = true; } } - Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); - if (set_active) + do { + is->distribution_hub_menu_reopen_requested = false; + Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); + } while (is->distribution_hub_menu_reopen_requested && + distribution_hub_menu_can_open_on_tile (tile_at (tile_x, tile_y), tile_x, tile_y)); + if (set_active) { is->named_tile_menu_active = false; + is->distribution_hub_menu_active = false; + is->distribution_hub_menu_reopen_requested = false; + } } void @@ -26113,7 +26931,7 @@ on_gain_city (Leader * leader, City * city, enum city_gain_reason reason) grant_nearby_wonders_to_city (city); } - if (is->current_config.enable_distribution_hub_districts) { + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { refresh_distribution_hubs_for_city (city); } } @@ -26137,8 +26955,7 @@ on_lose_city (Leader * leader, City * city, enum city_loss_reason reason) if (is->current_config.enable_wonder_districts) release_wonder_district_reservation (city); - } else if (is->current_config.enable_distribution_hub_districts) - is->distribution_hub_totals_dirty = true; + } } // Returns -1 if the location is unusable, 0-9 if it's usable but doesn't satisfy all criteria, and 10 if it couldn't be better @@ -26383,6 +27200,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 @@ -27265,6 +28084,11 @@ 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++) { @@ -27426,6 +28250,8 @@ patch_perform_interturn_in_main_loop () redraw = true; } } + if (redraw && ! reload_current_day_night_and_seasonal_images (&p_bic_data->Map.Renderer)) + redraw = false; if (redraw) p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); } @@ -27440,6 +28266,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; @@ -28068,6 +28898,7 @@ patch_Leader_do_production_phase (Leader * this) if (is->current_config.enable_distribution_hub_districts) { if (leader_can_build_district (this, DISTRIBUTION_HUB_DISTRICT_ID)) ai_update_distribution_hub_goal_for_leader (this); + ai_update_distribution_hub_city_selections_for_leader (this); } FOR_CITIES_OF (coi, this->ID) { @@ -28525,6 +29356,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 && @@ -28625,6 +29466,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; @@ -28888,10 +29744,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 && @@ -28916,10 +29773,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; + } } } } @@ -28943,6 +29803,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; @@ -29490,6 +30361,40 @@ patch_City_calc_tile_yield_while_gathering (City * this, int edx, YieldKind kind } } + if (is->current_config.enable_districts) { + Tile * tile = tile_at (tile_x, tile_y); + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && district_is_complete (tile, inst->district_id)) { + bool tile_improvement_rules = district_uses_tile_improvement_rules (inst->district_id); + bool city_gets_tile = tile_improvement_rules && (tile->Body.CityAreaID == this->Body.ID); + if (tile_improvement_rules && city_gets_tile) { + struct district_config * cfg = &is->district_configs[inst->district_id]; + int food_bonus = 0; + int shield_bonus = 0; + int gold_bonus = 0; + get_effective_district_yields (inst, cfg, &food_bonus, &shield_bonus, &gold_bonus, NULL, NULL, NULL); + if ((cfg->generated_resource_id >= 0) && + (cfg->generated_resource_flags & MF_YIELDS) && + district_can_generate_resource (this->Body.CivID, cfg)) { + Resource_Type * res = &p_bic_data->ResourceTypes[cfg->generated_resource_id]; + food_bonus += res->Food; + shield_bonus += res->Shield; + gold_bonus += res->Commerce; + } + if (kind == YK_FOOD) + tr = food_bonus; + else if (kind == YK_SHIELDS) + tr = shield_bonus; + else if (kind == YK_COMMERCE) + tr = gold_bonus; + } else { + tr = 0; + } + } + } + } + return tr; } @@ -30089,12 +30994,22 @@ patch_Leader_do_capture_city (Leader * this, int edx, City * city, bool involunt is->current_config.cities_with_mutual_district_receive_wonders && lost_small_wonder_count > 0) { reassign_shared_small_wonder_owners_after_city_loss (previous_owner, lost_small_wonders, lost_small_wonder_count); - } - - on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); - is->currently_capturing_city = NULL; - return tr; -} + } + + on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); + if (is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts) { + int old_civ_id = previous_owner->ID; + int new_civ_id = city->Body.CivID; + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (old_civ_id); + if (new_civ_id != old_civ_id) + recompute_distribution_hub_cities_for_civ (new_civ_id); + } + is->currently_capturing_city = NULL; + return tr; +} void __fastcall patch_City_raze (City * this, int edx, int civ_id_responsible, bool checking_elimination) @@ -30417,6 +31332,8 @@ patch_Map_place_scenario_things (Map * this) if (! any_natural_wonders) place_natural_wonders_on_map (); } + if (is->current_config.enable_custom_animations) + rebuild_tile_animation_rule_match_cache (); is->is_placing_scenario_things = false; } @@ -30438,9 +31355,12 @@ patch_Main_Screen_Form_open_quick_build_chooser (Main_Screen_Form * this, int ed { recompute_resources_if_necessary (); bool restore_named_tile_menu = is->named_tile_menu_active; + bool restore_distribution_hub_menu = is->distribution_hub_menu_active; is->named_tile_menu_active = false; + is->distribution_hub_menu_active = false; Main_Screen_Form_open_quick_build_chooser (this, __, city, mouse_x, mouse_y); is->named_tile_menu_active = restore_named_tile_menu; + is->distribution_hub_menu_active = restore_distribution_hub_menu; } int __fastcall @@ -30450,6 +31370,12 @@ patch_Context_Menu_get_selected_item_on_unit_rcm (Context_Menu * this) // click unit items which have been disabled by the mod so they can interrupt the queued actions of units that have no moves left. int index = this->Selected_Item; if (index >= 0) { + if (is->distribution_hub_menu_active && + distribution_hub_city_select_ui_enabled ()) { + Context_Menu_Item * item = &this->Items[index]; + if (handle_distribution_hub_menu_selection (item->Menu_Item_ID)) + return -1; + } if (is->current_config.enable_named_tiles && is->named_tile_menu_active) { Context_Menu_Item * item = &this->Items[index]; if (item->Menu_Item_ID == NAMED_TILE_MENU_ID) { @@ -30677,6 +31603,28 @@ patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR fi 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)); @@ -30832,8 +31780,14 @@ patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR fi is->current_config.enable_distribution_hub_districts && (is->distribution_hub_records.len > 0)) { serialize_aligned_text ("distribution_hub_records", &mod_data); - int entry_capacity = is->distribution_hub_records.len; - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); + int int_count = 1; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if (rec == NULL) + continue; + int_count += 5 + (int)rec->selected_city_ids.len; + } + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * int_count); int * out = chunk + 1; int written = 0; FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { @@ -30843,15 +31797,15 @@ patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR fi out[0] = rec->tile_x; out[1] = rec->tile_y; out[2] = rec->civ_id; - out += 3; + out[3] = rec->city_selection_mode; + out[4] = (int)rec->selected_city_ids.len; + out += 5; + FOR_TABLE_ENTRIES (selected_tei, &rec->selected_city_ids) { + *out++ = selected_tei.key; + } written++; } - chunk[0] = written; - int unused_entries = entry_capacity - written; - if (unused_entries > 0) { - int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); - mod_data.length -= trimmed_bytes; - } + chunk[0] = -written; } if (is->current_config.enable_districts && @@ -31016,6 +31970,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 @@ -31165,25 +32121,50 @@ patch_move_game_data (byte * buffer, bool save_else_load) // The day/night cycle sprite proxies will have been cleared in patch_load_scenario. They will not necessarily be set // up again in the usual way because Map_Renderer::load_images is not necessarily called when loading a save. The game // skips reloading all graphics when loading a save while in-game with another that uses the same graphics (possibly - // 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 (&p_bic_data->Map.Renderer); + // only the standard graphics; I didn't test). After all mod save chunks have been read, we reload the exact saved + // hour/season art in one pass. // 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; - + } else if (match_save_chunk_name (&cursor, "current_seasonal_cycle")) { is->current_seasonal_cycle = clamp (CS_SUMMER, CS_SPRING, *((int *)cursor)++); QueryPerformanceCounter (&is->last_seasonal_cycle_update_time); 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 @@ -31424,7 +32405,55 @@ patch_move_game_data (byte * buffer, bool save_else_load) int entry_count = *ints++; cursor = (byte *)ints; remaining_bytes -= (int)sizeof(int); - if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { + if (entry_count < 0) { + entry_count = -entry_count; + clear_distribution_hub_tables (); + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < 5 * (int)sizeof(int)) { + success = false; + break; + } + int x = *ints++; + int y = *ints++; + int civ_id = *ints++; + int selection_mode = *ints++; + int selected_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= 5 * (int)sizeof(int); + if ((selected_count < 0) || + (remaining_bytes < selected_count * (int)sizeof(int))) { + success = false; + break; + } + + Tile * tile = tile_at (x, y); + if ((tile != NULL) && (tile != p_null_tile)) { + on_distribution_hub_completed (tile, x, y); + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec != NULL) { + rec->civ_id = civ_id; + if (selection_mode == DHCSM_SPECIFIC_CITIES) + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + else + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + for (int selected_index = 0; selected_index < selected_count; selected_index++) { + int city_id = ints[selected_index]; + itable_insert (&rec->selected_city_ids, city_id, 1); + } + } + } + ints += selected_count; + cursor = (byte *)ints; + remaining_bytes -= selected_count * (int)sizeof(int); + } + if (success) { + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + } + } + else if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { clear_distribution_hub_tables (); success = true; for (int n = 0; n < entry_count; n++) { @@ -31445,6 +32474,10 @@ patch_move_game_data (byte * buffer, bool save_else_load) if (rec != NULL) rec->civ_id = civ_id; } + if (success) { + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + } } } if (! success) { @@ -31817,6 +32850,12 @@ patch_move_game_data (byte * buffer, bool save_else_load) free (seg); } + if ((! save_else_load) && + ((is->current_config.day_night_cycle_mode != DNCM_OFF) || + (is->current_config.seasonal_cycle_mode != SCM_OFF)) && + (is->day_night_cycle_img_state != IS_INIT_FAILED)) + reload_current_day_night_and_seasonal_images (&p_bic_data->Map.Renderer); + return tr; } @@ -32015,6 +33054,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); } @@ -32731,8 +33773,7 @@ draw_distribution_hub_yields (City_Form * city_form, Tile * tile, int tile_x, in if (rec == NULL) return; - City * anchor_city = get_connected_city_for_distribution_hub (rec); - if (! distribution_hub_accessible_to_city (rec, city_form->CurrentCity)) + if (! distribution_hub_distributes_to_city (rec, city_form->CurrentCity)) return; int food_yield = rec->food_yield; @@ -32805,6 +33846,127 @@ draw_distribution_hub_yields (City_Form * city_form, Tile * tile, int tile_x, in } } +void __fastcall +patch_Context_Menu_draw_item (Context_Menu * this, int edx, int item_index, int redraw) +{ + Context_Menu_draw_item (this, __, item_index, redraw); + + if ((this == NULL) || + ! is->distribution_hub_menu_active || + ! distribution_hub_city_select_ui_enabled () || + (item_index < 0) || + (item_index >= this->Item_Count)) + return; + + Context_Menu_Item * item = &this->Items[item_index]; + if (((item->Status & 1) == 0) || + (item->Menu_Item_ID < DISTRIBUTION_HUB_MENU_CITY_ID_BASE)) + return; + if (item->Image == &is->distribution_hub_menu_icon_sentinel) + return; + + struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); + if (rec == NULL) + return; + + City * city = get_city_ptr (item->Menu_Item_ID - DISTRIBUTION_HUB_MENU_CITY_ID_BASE); + if ((city == NULL) || + ! distribution_hub_distributes_to_city (rec, city)) + return; + + int food_yield = rec->food_yield; + int shield_yield = rec->shield_yield; + int total_yield = food_yield + shield_yield; + if (total_yield <= 0) + return; + + if (is->distribution_hub_icons_img_state == IS_UNINITED) + init_distribution_hub_icons (); + if (is->distribution_hub_icons_img_state != IS_OK) + return; + + Sprite * food_sprite = &is->distribution_hub_food_icon_small; + Sprite * shield_sprite = &is->distribution_hub_shield_icon_small; + + if (food_sprite->Width3 == 0) food_sprite = &is->distribution_hub_food_icon; + if (shield_sprite->Width3 == 0) shield_sprite = &is->distribution_hub_shield_icon; + + int food_width = food_sprite->Width3; + int shield_width = shield_sprite->Width3; + if ((food_width <= 0) && (shield_width > 0)) food_width = shield_width; + if ((shield_width <= 0) && (food_width > 0)) shield_width = food_width; + + int sprite_width = food_width > shield_width ? food_width : shield_width; + int sprite_height = food_sprite->Height; + if (sprite_height == 0) sprite_height = shield_sprite->Height; + if ((sprite_width <= 0) || (sprite_height <= 0)) + return; + + PCX_Image * canvas = &this->Base.Data.Canvas; + if (canvas->JGL.Image == NULL) + return; + + RECT menu_rect = canvas->JGL.Image->Image_Rect; + int menu_width = menu_rect.right - menu_rect.left; + int menu_height = menu_rect.bottom - menu_rect.top; + int item_height = this->ItemHeight; + if ((menu_width <= 0) || (menu_height <= 0) || (item_height <= 0)) + return; + + int row_index = 0; + int row_count = 0; + for (int n = 0; n < this->Item_Count; n++) + if ((this->Items[n].Status & 1) != 0) { + if (n < item_index) + row_index++; + row_count++; + } + if (row_count <= 0) + return; + + int vertical_pad = (menu_height - row_count * item_height) >> 1; + if (vertical_pad < 0) + vertical_pad = 0; + + int row_top = menu_rect.top + vertical_pad + row_index * item_height; + int pixel_y = row_top + ((item_height - sprite_height) >> 1); + if (pixel_y < menu_rect.top) + pixel_y = menu_rect.top; + + int right_pad = vertical_pad << 1; + if (right_pad < 6) + right_pad = 6; + + int icon_left = menu_rect.left + (menu_width >> 1); + int icon_right_edge = menu_rect.right - right_pad; + int icon_band_width = icon_right_edge - icon_left; + if (icon_band_width < sprite_width) + return; + + int spacing = sprite_width; + if ((total_yield > 1) && (sprite_width * total_yield > icon_band_width)) { + spacing = (icon_band_width - sprite_width) / (total_yield - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > sprite_width) + spacing = sprite_width; + } + + int used_width = sprite_width + spacing * (total_yield - 1); + int pixel_x = icon_right_edge - used_width; + if (pixel_x < icon_left) + pixel_x = icon_left; + + for (int i = 0; i < food_yield; i++) { + Sprite_draw (food_sprite, __, canvas, pixel_x + ((sprite_width - food_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < shield_yield; i++) { + Sprite_draw (shield_sprite, __, canvas, pixel_x + ((sprite_width - shield_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } +} + void __fastcall patch_City_Form_draw_yields_on_worked_tiles (City_Form * this) { @@ -34933,9 +36095,27 @@ draw_great_wall_district (Tile * tile, int tile_x, int tile_y, Map_Renderer * ma } void -draw_district_generated_resource_on_tile (Map_Renderer * this, Tile * tile, struct district_instance * inst, +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 && + (! is->tile_info_open) && + ((*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 = get_visible_non_subsumed_tile_resource (tile, inst, visible_to_civ_id); int district_resource = -1; @@ -34983,10 +36163,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; @@ -35075,6 +36288,26 @@ draw_district_on_tile (Map_Renderer * this, Tile * tile, struct district_instanc 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; @@ -35196,7 +36429,7 @@ draw_district_on_tile (Map_Renderer * this, Tile * tile, struct district_instanc return; } case DISTRIBUTION_HUB_DISTRICT_ID: - if (! is->current_config.enable_distribution_hub_districts) + if (! is->current_config.enable_districts || ! is->current_config.enable_distribution_hub_districts) return; draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); @@ -35288,32 +36521,59 @@ 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 && + (! is->tile_info_open) && + ((*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 @@ -36033,7 +37293,6 @@ recompute_district_and_distribution_hub_shields_for_city_view (City * city) int city_center_base_shields = City_calc_tile_yield_at (city, __, YK_SHIELDS, city_x, city_y); int total_district_shield_bonus = 0; calculate_city_center_district_bonus (city, NULL, &total_district_shield_bonus, NULL); - int city_center_district_shields = total_district_shield_bonus; FOR_DISTRICTS_AROUND (wai, city_x, city_y, true) { int district_id = wai.district_inst->district_id; @@ -36055,12 +37314,10 @@ recompute_district_and_distribution_hub_shields_for_city_view (City * city) // Distribution hub contribution is tracked separately for icon rendering. int distribution_hub_shields = 0; - if (is->current_config.enable_distribution_hub_districts) + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) get_distribution_hub_yields_for_city (city, NULL, &distribution_hub_shields); if (distribution_hub_shields < 0) distribution_hub_shields = 0; - if (distribution_hub_shields > city_center_district_shields) - distribution_hub_shields = city_center_district_shields; int standard_district_shields = total_district_shield_bonus - distribution_hub_shields; if (standard_district_shields < 0) @@ -37890,6 +39147,2101 @@ 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_hour = -1; + is->tile_animation_selected_season = -1; + is->tile_animation_selected_valid = false; +} + +void +clear_stale_custom_tile_animation_effects () +{ + if (p_main_screen_form == NULL) + return; + if (! is->tile_animation_selected_valid || (is->tile_animation_selected_next_index == 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 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) || (tile->Body.active_tile_effect == NULL)) + continue; + + int effect_id = tile->Body.active_tile_effect->V[2]; + if (! is_custom_tile_animation_effect (effect_id)) + continue; + + int animation_index = effect_id - is->tile_animation_effect_base; + if (is->tile_animation_selected_next_index[tile_index] != animation_index) + Tile_clear_animated_effect (tile); + } +} + +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 () +{ + if (is->saved_tile_count >= 0) + return; + + 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_hour = get_tile_animation_hour_for_match (); + is->tile_animation_selected_season = get_tile_animation_season_for_match (); + is->tile_animation_selected_valid = true; + clear_stale_custom_tile_animation_effects (); +} + +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; + int real_tile_count = (is->saved_tile_count >= 0) ? is->saved_tile_count : p_bic_data->Map.TileCount; + if (is->tile_animation_selected_tile_count != real_tile_count) + return true; + if (is->tile_animation_selected_animation_count != is->tile_animation_count) + return true; + if (is->tile_animation_selected_hour != get_tile_animation_hour_for_match ()) + return true; + if (is->tile_animation_selected_season != get_tile_animation_season_for_match ()) + 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; }