Alpine climate grf (unfinished)

This commit is contained in:
dP
2021-11-28 13:56:51 +03:00
committed by Pavel Stupnikov
parent 3c43becb61
commit 62ce8145e8
23 changed files with 3500 additions and 0 deletions

1707
grf/alpine/alpine.nml Normal file

File diff suppressed because it is too large Load Diff

179
grf/alpine/alpine.py Normal file
View File

@@ -0,0 +1,179 @@
import grf
import spectra
gen = grf.NewGRF(
b'CMAL',
'CityMania Alpine Landscape',
'Modified OpenGFX spritess for alpine climate.',
)
def replace_ground_sprites(sprite_id, file, x, y, **kw):
png = grf.ImageFile(file)
gen.add_sprite(grf.ReplaceSprites([(sprite_id, 19)]))
sprite = lambda *args, **kw: gen.add_sprite(grf.FileSprite(png, *args, **kw))
sprite( 0+x, y, 64, 31, xofs=-31, yofs= 0, **kw) #
sprite( 80+x, y, 64, 31, xofs=-31, yofs= 0, **kw) # W
sprite( 160+x, y, 64, 23, xofs=-31, yofs= 0, **kw) # S
sprite( 240+x, y, 64, 23, xofs=-31, yofs= 0, **kw) # S W
sprite( 320+x, y, 64, 31, xofs=-31, yofs= 0, **kw) # E
sprite( 398+x, y, 64, 31, xofs=-31, yofs= 0, **kw) # E W
sprite( 478+x, y, 64, 23, xofs=-31, yofs= 0, **kw) # E S
sprite( 558+x, y, 64, 23, xofs=-31, yofs= 0, **kw) # E S W
sprite( 638+x, y, 64, 39, xofs=-31, yofs= -8, **kw) # N
sprite( 718+x, y, 64, 39, xofs=-31, yofs= -8, **kw) # N W
sprite( 798+x, y, 64, 31, xofs=-31, yofs= -8, **kw) # N S
sprite( 878+x, y, 64, 31, xofs=-31, yofs= -8, **kw) # N S W
sprite( 958+x, y, 64, 39, xofs=-31, yofs= -8, **kw) # N E
sprite(1038+x, y, 64, 39, xofs=-31, yofs= -8, **kw) # N E W
sprite(1118+x, y, 64, 31, xofs=-31, yofs= -8, **kw) # N E S
sprite(1196+x, y, 64, 47, xofs=-31, yofs=-16, **kw) # N E W STEEP
sprite(1276+x, y, 64, 15, xofs=-31, yofs= 0, **kw) # E S W STEEP
sprite(1356+x, y, 64, 31, xofs=-31, yofs= -8, **kw) # N S W STEEP
sprite(1436+x, y, 64, 31, xofs=-31, yofs= -8, **kw) # N E S STEEP
def replace_shore_sprites(sprite_id, file, x, y, **kw):
png = grf.ImageFile(file)
gen.add_sprite(grf.ReplaceSprites([(sprite_id, 8)]))
sprite = lambda *args, **kw: gen.add_sprite(grf.FileSprite(png, *args, **kw))
sprite(320+x, y, 64, 31, xofs=-31, yofs= 0, **kw)
sprite( 80+x, y, 64, 31, xofs=-31, yofs= 0, **kw)
sprite(160+x, y, 64, 23, xofs=-31, yofs= 0, **kw)
sprite(638+x, y, 64, 39, xofs=-31, yofs=-8, **kw)
sprite(478+x, y, 64, 23, xofs=-31, yofs= 0, **kw)
sprite(958+x, y, 64, 39, xofs=-31, yofs=-8, **kw)
sprite(240+x, y, 64, 23, xofs=-31, yofs= 0, **kw)
sprite(718+x, y, 64, 39, xofs=-31, yofs=-8, **kw)
def replace_additional_rough_sprites(sprite_id, file, x, y, **kw):
png = grf.ImageFile(file)
gen.add_sprite(grf.ReplaceSprites([(sprite_id, 4)]))
sprite = lambda *args, **kw: gen.add_sprite(grf.FileSprite(png, *args, **kw))
sprite( x, y, 64, 31, xofs=-31, yofs=0, **kw)
sprite( 80+x, y, 64, 31, xofs=-31, yofs=0, **kw)
sprite(160+x, y, 64, 31, xofs=-31, yofs=0, **kw)
sprite(240+x, y, 64, 31, xofs=-31, yofs=0, **kw)
# Normal land
replace_ground_sprites(3981, 'gfx/grass_grid_temperate.gimp.png', 1, 1)
# bulldozed (bare) land and regeneration stages:
replace_ground_sprites(3924, 'gfx/bare03_grid.gimp.png', 1, 1)
replace_ground_sprites(3943, 'gfx/bare13_grid_temperate.gimp.png', 1, 1)
replace_ground_sprites(3962, 'gfx/bare23_grid_temperate.gimp.png', 1, 1)
# rough terrain
replace_ground_sprites(4000, 'gfx/rough_grid_temperate.gimp.png', 1, 1)
replace_additional_rough_sprites(4019, 'gfx/rough_grid_temperate.gimp.png', 1511, 1)
# rocky terrain
replace_ground_sprites(4023, 'gfx/rocks_grid_temperate.gimp.png', 1, 1)
# different snow densities:
replace_ground_sprites(4493, 'gfx/snow14_grid_alpine.gimp.png', 1, 1)
replace_ground_sprites(4512, 'gfx/snow24_grid_alpine.gimp.png', 1, 1)
replace_ground_sprites(4531, 'gfx/snow34_grid_alpine.gimp.png', 1, 1)
replace_ground_sprites(4550, 'gfx/snow_grid.gimp.png', 1, 1)
# shore sprites (replacing 16 seems to do these as well)
# replace_shore_sprites(4062, 'gfx/water/seashore_grid_temperate.gimp.png', 1, 1)
def replace_coastal_sprites(file, x, y, **kw):
png = grf.ImageFile(file)
gen.add_sprite(grf.ReplaceNewSprites(0x0d, 16))
sprite = lambda *args, **kw: gen.add_sprite(grf.FileSprite(png, *args, **kw))
sprite(1276+x, y, 64, 15, xofs=-31, yofs= 0, **kw)
sprite( 80+x, y, 64, 31, xofs=-31, yofs= 0, **kw)
sprite( 160+x, y, 64, 23, xofs=-31, yofs= 0, **kw)
sprite( 240+x, y, 64, 23, xofs=-31, yofs= 0, **kw)
sprite( 320+x, y, 64, 31, xofs=-31, yofs= 0, **kw)
sprite(1356+x, y, 64, 31, xofs=-31, yofs= -8, **kw)
sprite( 478+x, y, 64, 23, xofs=-31, yofs= 0, **kw)
sprite( 558+x, y, 64, 23, xofs=-31, yofs= 0, **kw)
sprite( 638+x, y, 64, 39, xofs=-31, yofs= -8, **kw)
sprite( 718+x, y, 64, 39, xofs=-31, yofs= -8, **kw)
sprite(1196+x, y, 64, 47, xofs=-31, yofs=-16, **kw)
sprite( 878+x, y, 64, 31, xofs=-31, yofs= -8, **kw)
sprite( 958+x, y, 64, 39, xofs=-31, yofs= -8, **kw)
sprite(1038+x, y, 64, 39, xofs=-31, yofs= -8, **kw)
sprite(1118+x, y, 64, 31, xofs=-31, yofs= -8, **kw)
sprite(1436+x, y, 64, 31, xofs=-31, yofs= -8, **kw)
replace_coastal_sprites('gfx/water/seashore_grid_temperate.gimp.png', 1, 1)
# CREEKS
# MEADOW
# spriteset (meadow_groundsprites, "gfx/meadow_grid_temperate.png") { tmpl_groundsprites(1, 1) }
# spriteset (meadow_transitions, "gfx/meadow_transitions.png") {
# tmpl_groundsprites(1, 0 * 64 + 1)
# tmpl_groundsprites(1, 1 * 64 + 1)
# tmpl_groundsprites(1, 2 * 64 + 1)
# tmpl_groundsprites(1, 3 * 64 + 1)
# tmpl_groundsprites(1, 4 * 64 + 1)
# tmpl_groundsprites(1, 5 * 64 + 1)
# tmpl_groundsprites(1, 6 * 64 + 1)
# tmpl_groundsprites(1, 7 * 64 + 1)
# tmpl_groundsprites(1, 8 * 64 + 1)
# tmpl_groundsprites(1, 9 * 64 + 1)
# tmpl_groundsprites(1, 10 * 64 + 1)
# tmpl_groundsprites(1, 11 * 64 + 1)
# tmpl_groundsprites(1, 12 * 64 + 1)
# tmpl_groundsprites(1, 13 * 64 + 1)
# tmpl_groundsprites(1, 14 * 64 + 1)
# tmpl_groundsprites(1, 15 * 64 + 1)
# }
# spritelayout meadow_groundsprites_default {
# ground {
# sprite: meadow_transitions(
# slope_to_sprite_offset(tile_slope)
# + (nearby_tile_object_type( 0, -1) == meadow && nearby_tile_object_type(-1, -1) == meadow &&nearby_tile_object_type(-1, 0) == meadow ? 0 : 19)
# + (nearby_tile_object_type(-1, 0) == meadow && nearby_tile_object_type(-1, 1) == meadow &&nearby_tile_object_type( 0, 1) == meadow ? 0 : 38)
# + (nearby_tile_object_type( 0, 1) == meadow && nearby_tile_object_type( 1, 1) == meadow &&nearby_tile_object_type( 1, 0) == meadow ? 0 : 76)
# + (nearby_tile_object_type( 1, 0) == meadow && nearby_tile_object_type( 1, -1) == meadow &&nearby_tile_object_type( 0, -1) == meadow ? 0 : 152)
# );
# }
# }
# spritelayout meadow_groundsprites_purchase {
# ground {
# sprite: meadow_groundsprites;
# }
# }
# switch (FEAT_OBJECTS, SELF, switch_meadow_groundsprites_default, [
# STORE_TEMP(slope_to_sprite_offset(tile_slope), 0)
# ]) {
# meadow_groundsprites_default;
# }
# item (FEAT_OBJECTS, meadow) {
# property {
# class: "FLMA";
# classname: string(STR_FLMA);
# name: string(STR_TEST_OBJECT);
# climates_available: ALL_CLIMATES;
# end_of_life_date: 0xFFFFFFFF;
# object_flags:bitmask(OBJ_FLAG_ALLOW_BRIDGE, OBJ_FLAG_ANYTHING_REMOVE, OBJ_FLAG_NO_FOUNDATIONS);
# size: [1,1];
# }
# graphics {
# default: meadow_groundsprites_default;
# purchase: meadow_groundsprites_purchase;
# tile_check: CB_RESULT_LOCATION_ALLOW;
# }
# }
gen.write('alpine.grf')

408
grf/alpine/alpine_gen.py Normal file
View File

@@ -0,0 +1,408 @@
print("""\
grf {
grfid: "CMAL";
name: string(STR_GRF_NAME);
desc: string(STR_GRF_DESCRIPTION);
version: 1;
min_compatible_version: 1;
}
template tmpl_groundsprites_flags(x, y, flags) {
// N E S W STEEP
[ 0+x, y, 64, 31, -31, 0, flags ] //
[ 80+x, y, 64, 31, -31, 0, flags ] // W
[ 160+x, y, 64, 23, -31, 0, flags ] // S
[ 240+x, y, 64, 23, -31, 0, flags ] // S W
[ 320+x, y, 64, 31, -31, 0, flags ] // E
[ 398+x, y, 64, 31, -31, 0, flags ] // E W
[ 478+x, y, 64, 23, -31, 0, flags ] // E S
[ 558+x, y, 64, 23, -31, 0, flags ] // E S W
[ 638+x, y, 64, 39, -31, -8, flags ] // N
[ 718+x, y, 64, 39, -31, -8, flags ] // N W
[ 798+x, y, 64, 31, -31, -8, flags ] // N S
[ 878+x, y, 64, 31, -31, -8, flags ] // N S W
[ 958+x, y, 64, 39, -31, -8, flags ] // N E
[1038+x, y, 64, 39, -31, -8, flags ] // N E W
[1118+x, y, 64, 31, -31, -8, flags ] // N E S
[1196+x, y, 64, 47, -31,-16, flags ] // N E W STEEP
[1276+x, y, 64, 15, -31, 0, flags ] // E S W STEEP
[1356+x, y, 64, 31, -31, -8, flags ] // N S W STEEP
[1436+x, y, 64, 31, -31, -8, flags ] // N E S STEEP
}
template tmpl_groundsprites(x, y) {
tmpl_groundsprites_flags(x, y, 0)
}
template tmpl_groundsprites_anim(x, y) {
tmpl_groundsprites_flags(x, y, ANIM)
}
template tmpl_level_ground(x, y) {
[ x, y, 64, 31, -31, 0 ]
}
template tmpl_rough(x, y) {
tmpl_level_ground( x, y)
tmpl_level_ground( 80+x, y)
tmpl_level_ground(160+x, y)
tmpl_level_ground(240+x, y)
}
template tmpl_additional_rough(x, y) {
tmpl_rough(1510+x, y)
}
template tmpl_16shore_tiles(x, y) {
[1276+x, y, 64, 15, -31, 0 ]
[ 80+x, y, 64, 31, -31, 0 ]
[ 160+x, y, 64, 23, -31, 0 ]
[ 240+x, y, 64, 23, -31, 0 ]
[ 320+x, y, 64, 31, -31, 0 ]
[1356+x, y, 64, 31, -31, -8 ]
[ 478+x, y, 64, 23, -31, 0 ]
[ 558+x, y, 64, 23, -31, 0 ]
[ 638+x, y, 64, 39, -31, -8 ]
[ 718+x, y, 64, 39, -31, -8 ]
[1196+x, y, 64, 47, -31,-16 ]
[ 878+x, y, 64, 31, -31, -8 ]
[ 958+x, y, 64, 39, -31, -8 ]
[1038+x, y, 64, 39, -31, -8 ]
[1118+x, y, 64, 31, -31, -8 ]
[1436+x, y, 64, 31, -31, -8 ]
}
template tmpl_8shore_tiles(x, y) {
[ 320+x, y, 64, 31, -31, 0 ]
[ 80+x, y, 64, 31, -31, 0 ]
[ 160+x, y, 64, 23, -31, 0 ]
[ 638+x, y, 64, 39, -31, -8 ]
[ 478+x, y, 64, 23, -31, 0 ]
[ 958+x, y, 64, 39, -31, -8 ]
[ 240+x, y, 64, 23, -31, 0 ]
[ 718+x, y, 64, 39, -31, -8 ]
}
template tmpl_tree_wide() {
[ 0, 0, 45, 80, -24, -73]
[ 50, 0, 45, 80, -24, -73]
[100, 0, 45, 80, -24, -73]
[150, 0, 45, 80, -24, -73]
[200, 0, 45, 80, -24, -73]
[250, 0, 45, 80, -24, -73]
[300, 0, 45, 80, -24, -73]
}
template tmpl_tree_narrow() {
[ 0, 0, 35, 80, -19, -73]
[ 40, 0, 35, 80, -19, -73]
[ 80, 0, 35, 80, -19, -73]
[120, 0, 35, 80, -19, -73]
[160, 0, 35, 80, -19, -73]
[200, 0, 35, 80, -19, -73]
[240, 0, 35, 80, -19, -73]
}
// Normal land:
replace (3981, "gfx/grass_grid_temperate.gimp.png") { tmpl_groundsprites(1, 1) }
// bulldozed (bare) land and regeneration stages:
replace (3924, "gfx/bare03_grid.gimp.png") { tmpl_groundsprites(1, 1) }
replace (3943, "gfx/bare13_grid_temperate.gimp.png") { tmpl_groundsprites(1, 1) }
replace (3962, "gfx/bare23_grid_temperate.gimp.png") { tmpl_groundsprites(1, 1) }
// rough terrain
replace (4000, "gfx/rough_grid_temperate.gimp.png") { tmpl_groundsprites(1, 1) }
replace (4019, "gfx/rough_grid_temperate.gimp.png") { tmpl_additional_rough(1, 1) }
// rocky terrain
replace (4023, "gfx/rocks_grid_temperate.gimp.png") { tmpl_groundsprites(1, 1) }
// different snow densities:
replace (4493, "gfx/snow14_grid_alpine.gimp.png") { tmpl_groundsprites(1, 1) }
replace (4512, "gfx/snow24_grid_alpine.gimp.png") { tmpl_groundsprites(1, 1) }
replace (4531, "gfx/snow34_grid_alpine.gimp.png") { tmpl_groundsprites(1, 1) }
replace (4550, "gfx/snow_grid.gimp.png") { tmpl_groundsprites(1, 1) }
// shore sprites
replace (4062, "gfx/water/seashore_grid_temperate.gimp.png") { tmpl_8shore_tiles(1, 1) }
replacenew (COAST_TILES, "gfx/water/seashore_grid_temperate.gimp.png") { tmpl_16shore_tiles(1, 1) }
// //Arctic trees: the following trees have snowy equivalents:
// base_graphics spr1709(1709, "trees/tree_01_conifer.gimp.png") { tmpl_tree_narrow() } // 1709 conifer (snowy: 1765)
// base_graphics spr1716(1716, "trees/tree_06_leaf.gimp.png") { tmpl_tree_narrow() } // 1716 leaf tree (snowy: 1772)
// base_graphics spr1723(1723, "trees/tree_07_leaf.gimp.png") { tmpl_tree_narrow() } // 1723 leaf tree (snowy: 1779)
// base_graphics spr1730(1730, "trees/tree_08_conifer.gimp.png") { tmpl_tree_narrow() } // 1730 conifer (snowy: 1786)
// base_graphics spr1737(1737, "trees/tree_09_conifer.gimp.png") { tmpl_tree_narrow() } // 1737 conifer (snowy: 1793)
// base_graphics spr1744(1744, "trees/tree_04_conifer.gimp.png") { tmpl_tree_narrow() } // 1744 conifer (snowy: 1800)
// base_graphics spr1751(1751, "trees/tree_05_conifer.gimp.png") { tmpl_tree_narrow() } // 1751 conifer (snowy: 1807)
// base_graphics spr1758(1758, "trees/tree_10_leaf.gimp.png") { tmpl_tree_narrow() } // 1758 leaf tree (snowy: 1814)
// // snowy trees
// base_graphics spr1765(1765, "trees/tree_01_snow_conifer.gimp.png") { tmpl_tree_narrow() } // 1765 snowy conifer (equiv of 1709)
// base_graphics spr1772(1772, "trees/tree_06_snow_leaf.gimp.png") { tmpl_tree_narrow() } // 1772 snowy leaf tree (equiv. of 1716)
// base_graphics spr1779(1779, "trees/tree_07_snow_leaf.gimp.png") { tmpl_tree_narrow() } // 1779 snowy leaf tree (equiv. of 1723)
// base_graphics spr1786(1786, "trees/tree_08_snow_conifer.gimp.png") { tmpl_tree_narrow() } // 1786 snowy conifer (equiv. of 1730)
// base_graphics spr1793(1793, "trees/tree_09_snow_conifer.gimp.png") { tmpl_tree_narrow() } // 1793 snowy conifer (equiv. of 1737)
// base_graphics spr1800(1800, "trees/tree_04_snow_conifer.gimp.png") { tmpl_tree_narrow() } // 1800 snowy conifer (equiv. of 1744)
// base_graphics spr1807(1807, "trees/tree_05_snow_conifer.gimp.png") { tmpl_tree_narrow() } // 1807 snowy conifer (equiv. of 1751)
// base_graphics spr1814(1814, "trees/tree_10_snow_leaf.gimp.png") { tmpl_tree_narrow() } // 1814 snowy leaf tree (equiv. of 1758)
spriteset (meadow_groundsprites, "gfx/meadow_grid_temperate.png") { tmpl_groundsprites(1, 1) }
spriteset (meadow_transitions, "gfx/meadow_transitions.png") {
tmpl_groundsprites(1, 0 * 64 + 1)
tmpl_groundsprites(1, 1 * 64 + 1)
tmpl_groundsprites(1, 2 * 64 + 1)
tmpl_groundsprites(1, 3 * 64 + 1)
tmpl_groundsprites(1, 4 * 64 + 1)
tmpl_groundsprites(1, 5 * 64 + 1)
tmpl_groundsprites(1, 6 * 64 + 1)
tmpl_groundsprites(1, 7 * 64 + 1)
tmpl_groundsprites(1, 8 * 64 + 1)
tmpl_groundsprites(1, 9 * 64 + 1)
tmpl_groundsprites(1, 10 * 64 + 1)
tmpl_groundsprites(1, 11 * 64 + 1)
tmpl_groundsprites(1, 12 * 64 + 1)
tmpl_groundsprites(1, 13 * 64 + 1)
tmpl_groundsprites(1, 14 * 64 + 1)
tmpl_groundsprites(1, 15 * 64 + 1)
}
spritelayout meadow_groundsprites_default {
ground {
sprite: meadow_transitions(
slope_to_sprite_offset(tile_slope)
+ (nearby_tile_object_type( 0, -1) == meadow && nearby_tile_object_type(-1, -1) == meadow &&nearby_tile_object_type(-1, 0) == meadow ? 0 : 19)
+ (nearby_tile_object_type(-1, 0) == meadow && nearby_tile_object_type(-1, 1) == meadow &&nearby_tile_object_type( 0, 1) == meadow ? 0 : 38)
+ (nearby_tile_object_type( 0, 1) == meadow && nearby_tile_object_type( 1, 1) == meadow &&nearby_tile_object_type( 1, 0) == meadow ? 0 : 76)
+ (nearby_tile_object_type( 1, 0) == meadow && nearby_tile_object_type( 1, -1) == meadow &&nearby_tile_object_type( 0, -1) == meadow ? 0 : 152)
);
}
}
spritelayout meadow_groundsprites_purchase {
ground {
sprite: meadow_groundsprites;
}
}
switch (FEAT_OBJECTS, SELF, switch_meadow_groundsprites_default, [
STORE_TEMP(slope_to_sprite_offset(tile_slope), 0)
]) {
meadow_groundsprites_default;
}
item (FEAT_OBJECTS, meadow) {
property {
class: "FLMA";
classname: string(STR_FLMA);
name: string(STR_TEST_OBJECT);
climates_available: ALL_CLIMATES;
end_of_life_date: 0xFFFFFFFF;
object_flags:bitmask(OBJ_FLAG_ALLOW_BRIDGE, OBJ_FLAG_ANYTHING_REMOVE, OBJ_FLAG_NO_FOUNDATIONS);
size: [1,1];
}
graphics {
default: meadow_groundsprites_default;
purchase: meadow_groundsprites_purchase;
tile_check: CB_RESULT_LOCATION_ALLOW;
}
}
spriteset (creek_groundsprites, "gfx/rivers.png") {
tmpl_groundsprites_anim(1, 0 * 64 + 1)
tmpl_groundsprites_anim(1, 1 * 64 + 1)
tmpl_groundsprites_anim(1, 2 * 64 + 1)
tmpl_groundsprites_anim(1, 3 * 64 + 1)
tmpl_groundsprites_anim(1, 4 * 64 + 1)
tmpl_groundsprites_anim(1, 5 * 64 + 1)
tmpl_groundsprites_anim(1, 6 * 64 + 1)
tmpl_groundsprites_anim(1, 7 * 64 + 1)
tmpl_groundsprites_anim(1, 8 * 64 + 1)
tmpl_groundsprites_anim(1, 9 * 64 + 1)
tmpl_groundsprites_anim(1, 10 * 64 + 1)
tmpl_groundsprites_anim(1, 11 * 64 + 1)
tmpl_groundsprites_anim(1, 12 * 64 + 1)
tmpl_groundsprites_anim(1, 13 * 64 + 1)
tmpl_groundsprites_anim(1, 14 * 64 + 1)
tmpl_groundsprites_anim(1, 15 * 64 + 1)
tmpl_groundsprites_anim(1, 16 * 64 + 1)
tmpl_groundsprites_anim(1, 17 * 64 + 1)
tmpl_groundsprites_anim(1, 18 * 64 + 1)
tmpl_groundsprites_anim(1, 19 * 64 + 1)
tmpl_groundsprites_anim(1, 20 * 64 + 1)
tmpl_groundsprites_anim(1, 21 * 64 + 1)
tmpl_groundsprites_anim(1, 22 * 64 + 1)
tmpl_groundsprites_anim(1, 23 * 64 + 1)
tmpl_groundsprites_anim(1, 24 * 64 + 1)
tmpl_groundsprites_anim(1, 25 * 64 + 1)
tmpl_groundsprites_anim(1, 26 * 64 + 1)
tmpl_groundsprites_anim(1, 27 * 64 + 1)
tmpl_groundsprites_anim(1, 28 * 64 + 1)
tmpl_groundsprites_anim(1, 29 * 64 + 1)
tmpl_groundsprites_anim(1, 30 * 64 + 1)
tmpl_groundsprites_anim(1, 31 * 64 + 1)
tmpl_groundsprites_anim(1, 32 * 64 + 1)
tmpl_groundsprites_anim(1, 33 * 64 + 1)
tmpl_groundsprites_anim(1, 34 * 64 + 1)
tmpl_groundsprites_anim(1, 35 * 64 + 1)
tmpl_groundsprites_anim(1, 36 * 64 + 1)
tmpl_groundsprites_anim(1, 37 * 64 + 1)
tmpl_groundsprites_anim(1, 38 * 64 + 1)
tmpl_groundsprites_anim(1, 39 * 64 + 1)
tmpl_groundsprites_anim(1, 40 * 64 + 1)
tmpl_groundsprites_anim(1, 41 * 64 + 1)
tmpl_groundsprites_anim(1, 42 * 64 + 1)
tmpl_groundsprites_anim(1, 43 * 64 + 1)
tmpl_groundsprites_anim(1, 44 * 64 + 1)
tmpl_groundsprites_anim(1, 45 * 64 + 1)
tmpl_groundsprites_anim(1, 46 * 64 + 1)
tmpl_groundsprites_anim(1, 47 * 64 + 1)
tmpl_groundsprites_anim(1, 48 * 64 + 1)
tmpl_groundsprites_anim(1, 49 * 64 + 1)
tmpl_groundsprites_anim(1, 50 * 64 + 1)
tmpl_groundsprites_anim(1, 51 * 64 + 1)
tmpl_groundsprites_anim(1, 52 * 64 + 1)
tmpl_groundsprites_anim(1, 53 * 64 + 1)
tmpl_groundsprites_anim(1, 54 * 64 + 1)
tmpl_groundsprites_anim(1, 55 * 64 + 1)
tmpl_groundsprites_anim(1, 56 * 64 + 1)
tmpl_groundsprites_anim(1, 57 * 64 + 1)
tmpl_groundsprites_anim(1, 58 * 64 + 1)
tmpl_groundsprites_anim(1, 59 * 64 + 1)
tmpl_groundsprites_anim(1, 60 * 64 + 1)
tmpl_groundsprites_anim(1, 61 * 64 + 1)
tmpl_groundsprites_anim(1, 62 * 64 + 1)
tmpl_groundsprites_anim(1, 63 * 64 + 1)
tmpl_groundsprites_anim(1, 64 * 64 + 1)
tmpl_groundsprites_anim(1, 65 * 64 + 1)
tmpl_groundsprites_anim(1, 66 * 64 + 1)
tmpl_groundsprites_anim(1, 67 * 64 + 1)
tmpl_groundsprites_anim(1, 68 * 64 + 1)
tmpl_groundsprites_anim(1, 69 * 64 + 1)
tmpl_groundsprites_anim(1, 70 * 64 + 1)
tmpl_groundsprites_anim(1, 71 * 64 + 1)
tmpl_groundsprites_anim(1, 72 * 64 + 1)
tmpl_groundsprites_anim(1, 73 * 64 + 1)
tmpl_groundsprites_anim(1, 74 * 64 + 1)
tmpl_groundsprites_anim(1, 75 * 64 + 1)
tmpl_groundsprites_anim(1, 76 * 64 + 1)
tmpl_groundsprites_anim(1, 77 * 64 + 1)
tmpl_groundsprites_anim(1, 78 * 64 + 1)
tmpl_groundsprites_anim(1, 79 * 64 + 1)
tmpl_groundsprites_anim(1, 80 * 64 + 1)
}
spritelayout creek_groundsprites_default(n, tile_height) {
ground {
sprite:
(climate == CLIMATE_ARCTIC && tile_height > snowline_height - 2 ?
GROUNDSPRITE_SNOW + min(tile_height - snowline_height - 2, 0) * 19
: (terrain_type == TILETYPE_DESERT ? GROUNDSPRITE_DESERT : GROUNDSPRITE_NORMAL))
+ slope_to_sprite_offset(tile_slope);
}
childsprite {
sprite: creek_groundsprites(
slope_to_sprite_offset(tile_slope) + 19 * n
);
always_draw: 1;
}
}
spritelayout creek_groundsprites_purchase(n) {
ground { sprite: GROUNDSPRITE_NORMAL; }
childsprite {
sprite: creek_groundsprites(19 * n);
always_draw: 1;
}
}
""")
print('spriteset (mine_number_sprites, "gfx/mine_numbers.png") {')
for i in range(10):
print(f' [{i * 12}, 0, 12, 16, -6, 8]')
print('}')
print(f""" \
spritelayout minefield_sprites(n) {{
ground {{ sprite: GROUNDSPRITE_NORMAL + slope_to_sprite_offset(tile_slope); }}
childsprite {{
sprite: mine_number_sprites(n);
always_draw: 1;
}}
}}
""")
print(f""" \
item (FEAT_OBJECTS, minefield) {{
property {{
class: "MINE";
classname: string(STR_MINEFIELD);
name: string(STR_TEST_OBJECT);
climates_available: ALL_CLIMATES;
end_of_life_date: 0xFFFFFFFF;
object_flags:bitmask(OBJ_FLAG_ANYTHING_REMOVE, OBJ_FLAG_NO_FOUNDATIONS);
size: [1,1];
num_views: 2;
}}
graphics {{
default: minefield_sprites(
(
(nearby_tile_object_type(-1, -1) == minefield) +
(nearby_tile_object_type(-1, 0) == minefield) +
(nearby_tile_object_type(-1, 1) == minefield) +
(nearby_tile_object_type(0, -1) == minefield) +
(nearby_tile_object_type(0, 1) == minefield) +
(nearby_tile_object_type(1, -1) == minefield) +
(nearby_tile_object_type(1, 0) == minefield) +
(nearby_tile_object_type(1, 1) == minefield)
) == 8 ? 9 : (
(nearby_tile_object_view(-1, -1)) +
(nearby_tile_object_view(-1, 0)) +
(nearby_tile_object_view(-1, 1)) +
(nearby_tile_object_view(0, -1)) +
(nearby_tile_object_view(0, 1)) +
(nearby_tile_object_view(0, 0)) +
(nearby_tile_object_view(1, -1)) +
(nearby_tile_object_view(1, 0)) +
(nearby_tile_object_view(1, 1))
)
);
purchase: minefield_sprites(9);
tile_check: CB_RESULT_LOCATION_ALLOW;
}}
}}
""")
for i in range(81):
print(f"""\
item (FEAT_OBJECTS, rivers_{i}) {{
property {{
class: "CREE";
classname: string(STR_CREEK);
name: string(STR_TEST_OBJECT);
climates_available: ALL_CLIMATES;
end_of_life_date: 0xFFFFFFFF;
object_flags:bitmask(OBJ_FLAG_ALLOW_BRIDGE, OBJ_FLAG_ANYTHING_REMOVE, OBJ_FLAG_NO_FOUNDATIONS);
size: [1,1];
}}
graphics {{
default: creek_groundsprites_default({i}, nearby_tile_height(0, 0));
purchase: creek_groundsprites_purchase({i});
tile_check: CB_RESULT_LOCATION_ALLOW;
}}
}}
""")

302
grf/alpine/gen_sprites.py Normal file
View File

@@ -0,0 +1,302 @@
from PIL import Image, ImageDraw
import numpy as np
import math
import os
import spectra
SAFE_COLORS = list(range(1, 0xD7))
f = open("../ttd-newgrf-dos.gpl")
while f.readline().strip() != "#":
pass
colors = []
for _ in range(256):
try:
r, g, b, _, i = f.readline().split()
except ValueError:
break
c = spectra.rgb(float(r) / 255., float(g) / 255., float(b) / 255.)
# if c in SAFE_COLORS:
colors.append((int(i), c))
def color_distance(c1, c2):
rmean = (c1.rgb[0] + c2.rgb[0]) / 2.
r = c1.rgb[0] - c2.rgb[0]
g = c1.rgb[1] - c2.rgb[1]
b = c1.rgb[2] - c2.rgb[2]
return math.sqrt(
((2 + rmean) * r * r) +
4 * g * g +
(3 - rmean) * b * b)
def find_best_color(x):
mj, md = 0, 1e100
for j, c in colors:
if j not in SAFE_COLORS:
continue
d = color_distance(x, c)
if d < md:
mj, md = j, d
return mj
def gen_recolor(color_func):
out = np.arange(256, dtype=np.uint8)
for i, c in colors:
if i not in SAFE_COLORS:
continue
out[i] = find_best_color(color_func(c))
return out
def gen_tint(tint, ratio):
return lambda x: find_best_color(x.blend(tint, ratio=ratio))
def gen_brightness(level):
def func(x):
if level > 0:
return x.brighten(amount=2.56 * level)
else:
return x.darken(amount=-2.56 * level)
return gen_recolor(func)
# def gen_land_recolor():
# def func(x):
# if 2 * g > r + b:
# x = x.blend(spectra.rgb(0.7, 1, 0), ratio=0.2)
# else:
# x = x.saturate(20)
# return x
# return gen_recolor(func)
def gen_land_recolor():
def func(x):
r, g, b = x.rgb
if 2 * g > r + b:
x = x.blend(spectra.rgb(0.7, 1, 0), ratio=0.05)
x = x.saturate(20)
# # elif 3 * b > r + g:
# # x = x.blend(spectra.rgb(0, 0, 1), ratio=0.3)
# # x = x.blend(spectra.rgb(1, 0, 1), ratio=0.5)
# # x = x.saturate(40)
else:
x = x.blend(spectra.rgb(0.7, 1, 0), ratio=0.05)
x = x.saturate(5)
return x
return gen_recolor(func)
def remap_file(f_in, f_out, palmap):
print(f"Converting {f_out}...")
im = Image.open(f_in)
data = np.array(im)
data = palmap[data]
im2 = Image.fromarray(data)
im2.putpalette(im.getpalette())
im2.save(f_out)
SOURCE_DIR = "/home/pavels/Projects/cmclient/local/ogfx-landscape-1.1.2-source/src/gfx"
DEST_DIR = "gfx"
land_palmap = gen_land_recolor()
for fname in ("grass_grid_temperate.gimp.png",
"bare03_grid.gimp.png",
"bare13_grid_temperate.gimp.png",
"bare23_grid_temperate.gimp.png",
"rough_grid_temperate.gimp.png",
"rough_grid_temperate.gimp.png",
"rocks_grid_temperate.gimp.png",
"snow14_grid_alpine.gimp.png",
"snow24_grid_alpine.gimp.png",
"snow34_grid_alpine.gimp.png",
"snow_grid.gimp.png",
"water/seashore_grid_temperate.gimp.png",
):
remap_file(os.path.join(SOURCE_DIR, fname), os.path.join(DEST_DIR, fname), land_palmap)
# SOURCE_DIR = "/home/pavels/Builds/OpenGFX"
# TREES = [
# "sprites/png/trees/arctic/tree_01_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_06_leaf.gimp.png",
# "sprites/png/trees/arctic/tree_07_leaf.gimp.png",
# "sprites/png/trees/arctic/tree_08_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_09_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_04_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_05_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_10_leaf.gimp.png",
# "sprites/png/trees/arctic/tree_01_snow_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_06_snow_leaf.gimp.png",
# "sprites/png/trees/arctic/tree_07_snow_leaf.gimp.png",
# "sprites/png/trees/arctic/tree_08_snow_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_09_snow_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_04_snow_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_05_snow_conifer.gimp.png",
# "sprites/png/trees/arctic/tree_10_snow_leaf.gimp.png",
# ]
# for fname in TREES:
def meadow_recolor(x):
x = x.blend(spectra.rgb(0.7, 1, 0), ratio=0.2)
return x.blend(spectra.rgb(1, 1, 0), ratio=0.4)
def half_meadow_recolor(x):
x = x.blend(spectra.rgb(0.7, 1, 0), ratio=0.2)
return x.blend(spectra.rgb(1, 1, 0), ratio=0.2)
remap_file(os.path.join(SOURCE_DIR, "grass_grid_temperate.gimp.png"), os.path.join(DEST_DIR, "meadow_grid_temperate.png"), gen_recolor(meadow_recolor))
remap_file(os.path.join(SOURCE_DIR, "grass_grid_temperate.gimp.png"), os.path.join(DEST_DIR, "half_meadow_grid_temperate.png"), gen_recolor(half_meadow_recolor))
# Generate meadow transition tiles
im = Image.open(os.path.join(SOURCE_DIR, "grass_grid_temperate.gimp.png"))
din = np.array(im)
GROUND_SPRITES = [
# N E S W STEEP
[ 0 + 1, 1, 64, 31, -31, 0, 15, 15], #
[ 80 + 1, 1, 64, 31, -31, 0, 7, 15], # W
[ 160 + 1, 1, 64, 23, -31, 0, 15, 15], # S
[ 240 + 1, 1, 64, 23, -31, 0, 7, 15], # S W
[ 320 + 1, 1, 64, 31, -31, 0, 15, 7], # E
[ 398 + 1, 1, 64, 31, -31, 0, 7, 7], # E W
[ 478 + 1, 1, 64, 23, -31, 0, 15, 7], # E S
[ 558 + 1, 1, 64, 23, -31, 0, 7, 7], # E S W
[ 638 + 1, 1, 64, 39, -31, -8, 23, 23], # N
[ 718 + 1, 1, 64, 39, -31, -8, 15, 23], # N W
[ 798 + 1, 1, 64, 31, -31, -8, 23, 23], # N S
[ 878 + 1, 1, 64, 31, -31, -8, 15, 23], # N S W
[ 958 + 1, 1, 64, 39, -31, -8, 23, 15], # N E
[1038 + 1, 1, 64, 39, -31, -8, 15, 15], # N E W
[1118 + 1, 1, 64, 31, -31, -8, 23, 15], # N E S
[1196 + 1, 1, 64, 47, -31,-16, 23, 23], # N E W STEEP
[1276 + 1, 1, 64, 15, -31, 0, 7, 7], # E S W STEEP
[1356 + 1, 1, 64, 31, -31, -8, 7, 23], # N S W STEEP
[1436 + 1, 1, 64, 31, -31, -8, 23, 7], # N E S STEEP
]
# dout = np.zeros((64 * 8, 31 + 47 + 15), dtype=np.uint8)
# dout = np.zeros((31 + 47 + 15, 64 * 8), dtype=np.uint8)
# dout = np.zeros((64 * 16, im.width), dtype=np.uint8)
# for i in range (16):
# print(f'Generating sprite row {i}/16...')
# for j, (ox, oy, w, h, _ox, _oy, h1, h2) in enumerate(GROUND_SPRITES):
# hn1 = 2. * (h1 + .5 - h / 2.) / h
# hn2 = 2. * (h2 + .5 - h / 2.) / h
# iflags = [i & (1 << k) for k in range(4)]
# ut, uc, ub = i & 1, (i & 2) / 2, (i & 4) / 4
# for y in range(0, h):
# for x in range(0, w):
# c = din[y + oy, x + ox]
# if not c: continue
# xn = 2. * (x + .5 - w / 2.) / w
# yn = 2. * (y + .5 - h / 2.) / h
# dn = math.hypot(xn, yn + 1)
# ds = math.hypot(xn, yn - 1)
# de = math.hypot(xn - 1, yn - hn1)
# dw = math.hypot(xn + 1, yn - hn2)
# f = min(d if fl else 2. for d, fl in zip([dn, de, ds, dw], iflags))
# f = min(f * 1.44, 1.)
# bc = spectra.rgb(f, 1, 0)
# c = colors[c][1].blend(bc, ratio=0.2 + 0.1 * f)
# dout[oy + y + 64 * i, ox + x] = find_best_color(c)
# im2 = Image.fromarray(dout)
# im2.putpalette(im.getpalette())
# im2.save(os.path.join(DEST_DIR, "meadow_transitions.png"))
dmask = np.vectorize(lambda x: 0 if x and x != 0xFF else 0xFF)(din).astype('uint8')
immask = Image.fromarray(dmask, mode="L")
# immask.save(os.path.join(DEST_DIR, "mask.png"))
dout = np.zeros((64 * 81, im.width), dtype=np.uint8)
im2 = Image.fromarray(dout)
im2.putpalette(im.getpalette())
imd2 = ImageDraw.Draw(im2)
# draw.line((0, 0) + im.size, fill=128)
# draw.line((0, im.size[1], im.size[0], 0), fill=128)
def draw_bezier(imd, fill, width, a, b, c):
N, M = 11, 2
lerp = lambda x, y, t: t * x + (N - t) * y
lerp2m = lambda a, b, t: (lerp(a[0], (a[0] + b[0]) // 2, t), lerp(a[1], (a[1] + b[1]) // 2, t))
lerp2 = lambda a, b, t: (lerp(a[0], b[0], t), lerp(a[1], b[1], t))
for t in range(-M, N + M + 1):
p0 = lerp2m(a, b, t)
p1 = lerp2m(c, b, N - t)
pf = lerp2(p0, p1, t)
x = pf[0] // N // N
y = pf[1] // N // N
imd.ellipse((x - width, y - width, x + width, y + width), fill=fill)
for i in range (81):
print(f'Generating rivers sprite row {i + 1}/81...')
points = (i % 3, (i // 3) % 3, (i // 9) % 3, (i // 27))
inp = []
outp = []
for ii, p in enumerate(points):
if p == 2: outp.append(ii)
elif p == 1: inp.append(ii)
if not inp: inp = [4]
if not outp: outp = [4]
for j, (ox, oy, w, h, _ox, _oy, h1, h2) in enumerate(GROUND_SPRITES):
xx, yy = ox, oy + 64 * i
# # tile outline
# corners = ((0, h1), (w / 2, 0), (w, h2), (w / 2, h))
# for ii in range(len(corners)):
# x1, y1 = corners[ii]
# x2, y2 = corners[(ii + 1) % len(corners)]
# imd2.line(((x1 + xx, y1 + yy), (x2 + xx, y2 + yy)), fill=0xA9, width=1)
# hn1 = 2. * (h1 + .5 - h / 2.) / h
# hn2 = 2. * (h2 + .5 - h / 2.) / h
wc = w // 4
edges = ((wc, h1 / 2), (w - wc, h2 / 2), (w - wc, (h + h2) / 2), (wc, (h + h1) / 2), (w / 2, h / 2))
center = (w / 2 + xx, h / 2 + yy)
# if not inp:
# continue
for ii in inp:
for oo in outp:
xy = ((edges[ii][0] + xx, edges[ii][1] + yy), (edges[oo][0] + xx, edges[oo][1] + yy))
draw_bezier(imd2, 0x42, 4, xy[0], center, xy[1])
for ii in inp:
for oo in outp:
xy = ((edges[ii][0] + xx, edges[ii][1] + yy), (edges[oo][0] + xx, edges[oo][1] + yy))
draw_bezier(imd2, 0xF5, 3, xy[0], center, xy[1])
# # mark out
# for oo in outp:
# x, y = edges[oo][0] + xx, edges[oo][1] + yy
# width = 3
# imd2.ellipse((x - width, y - width, x + width, y + width), fill=0x42)
imd2.bitmap((0, i * 64), immask, fill=0)
# iflags = [i & (1 << k) for k in range(4)]
# ut, uc, ub = i & 1, (i & 2) / 2, (i & 4) / 4
# for y in range(0, h):
# for x in range(0, w):
# c = din[y + oy, x + ox]
# if not c: continue
# xn = 2. * (x + .5 - w / 2.) / w
# yn = 2. * (y + .5 - h / 2.) / h
# dn = math.hypot(xn, yn + 1)
# ds = math.hypot(xn, yn - 1)
# de = math.hypot(xn - 1, yn - hn1)
# dw = math.hypot(xn + 1, yn - hn2)
# f = min(d if fl else 2. for d, fl in zip([dn, de, ds, dw], iflags))
# f = min(f * 1.44, 1.)
# bc = spectra.rgb(f, 1, 0)
# c = colors[c][1].blend(bc, ratio=0.2 + 0.1 * f)
# dout[oy + y + 64 * i, ox + x] = find_best_color(c)
# im2 = Image.fromarray(dout)
# im2.putpalette(im.getpalette())
im2.save(os.path.join(DEST_DIR, "rivers.png"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
grf/alpine/gfx/mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
grf/alpine/gfx/rivers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

336
grf/alpine/grf.py Normal file
View File

@@ -0,0 +1,336 @@
import math
from PIL import Image, ImageDraw
from nml.spriteencoder import SpriteEncoder
import spectra
import struct
import numpy as np
to_spectra = lambda r, g, b: spectra.rgb(float(r) / 255., float(g) / 255., float(b) / 255.)
# working with DOS palette only
PALETTE = (0, 0, 255, 16, 16, 16, 32, 32, 32, 48, 48, 48, 64, 64, 64, 80, 80, 80, 100, 100, 100, 116, 116, 116, 132, 132, 132, 148, 148, 148, 168, 168, 168, 184, 184, 184, 200, 200, 200, 216, 216, 216, 232, 232, 232, 252, 252, 252, 52, 60, 72, 68, 76, 92, 88, 96, 112, 108, 116, 132, 132, 140, 152, 156, 160, 172, 176, 184, 196, 204, 208, 220, 48, 44, 4, 64, 60, 12, 80, 76, 20, 96, 92, 28, 120, 120, 64, 148, 148, 100, 176, 176, 132, 204, 204, 168, 72, 44, 4, 88, 60, 20, 104, 80, 44, 124, 104, 72, 152, 132, 92, 184, 160, 120, 212, 188, 148, 244, 220, 176, 64, 0, 4, 88, 4, 16, 112, 16, 32, 136, 32, 52, 160, 56, 76, 188, 84, 108, 204, 104, 124, 220, 132, 144, 236, 156, 164, 252, 188, 192, 252, 208, 0, 252, 232, 60, 252, 252, 128, 76, 40, 0, 96, 60, 8, 116, 88, 28, 136, 116, 56, 156, 136, 80, 176, 156, 108, 196, 180, 136, 68, 24, 0, 96, 44, 4, 128, 68, 8, 156, 96, 16, 184, 120, 24, 212, 156, 32, 232, 184, 16, 252, 212, 0, 252, 248, 128, 252, 252, 192, 32, 4, 0, 64, 20, 8, 84, 28, 16, 108, 44, 28, 128, 56, 40, 148, 72, 56, 168, 92, 76, 184, 108, 88, 196, 128, 108, 212, 148, 128, 8, 52, 0, 16, 64, 0, 32, 80, 4, 48, 96, 4, 64, 112, 12, 84, 132, 20, 104, 148, 28, 128, 168, 44, 28, 52, 24, 44, 68, 32, 60, 88, 48, 80, 104, 60, 104, 124, 76, 128, 148, 92, 152, 176, 108, 180, 204, 124, 16, 52, 24, 32, 72, 44, 56, 96, 72, 76, 116, 88, 96, 136, 108, 120, 164, 136, 152, 192, 168, 184, 220, 200, 32, 24, 0, 56, 28, 0, 72, 40, 4, 88, 52, 12, 104, 64, 24, 124, 84, 44, 140, 108, 64, 160, 128, 88, 76, 40, 16, 96, 52, 24, 116, 68, 40, 136, 84, 56, 164, 96, 64, 184, 112, 80, 204, 128, 96, 212, 148, 112, 224, 168, 128, 236, 188, 148, 80, 28, 4, 100, 40, 20, 120, 56, 40, 140, 76, 64, 160, 100, 96, 184, 136, 136, 36, 40, 68, 48, 52, 84, 64, 64, 100, 80, 80, 116, 100, 100, 136, 132, 132, 164, 172, 172, 192, 212, 212, 224, 40, 20, 112, 64, 44, 144, 88, 64, 172, 104, 76, 196, 120, 88, 224, 140, 104, 252, 160, 136, 252, 188, 168, 252, 0, 24, 108, 0, 36, 132, 0, 52, 160, 0, 72, 184, 0, 96, 212, 24, 120, 220, 56, 144, 232, 88, 168, 240, 128, 196, 252, 188, 224, 252, 16, 64, 96, 24, 80, 108, 40, 96, 120, 52, 112, 132, 80, 140, 160, 116, 172, 192, 156, 204, 220, 204, 240, 252, 172, 52, 52, 212, 52, 52, 252, 52, 52, 252, 100, 88, 252, 144, 124, 252, 184, 160, 252, 216, 200, 252, 244, 236, 72, 20, 112, 92, 44, 140, 112, 68, 168, 140, 100, 196, 168, 136, 224, 200, 176, 248, 208, 184, 255, 232, 208, 252, 60, 0, 0, 92, 0, 0, 128, 0, 0, 160, 0, 0, 196, 0, 0, 224, 0, 0, 252, 0, 0, 252, 80, 0, 252, 108, 0, 252, 136, 0, 252, 164, 0, 252, 192, 0, 252, 220, 0, 252, 252, 0, 204, 136, 8, 228, 144, 4, 252, 156, 0, 252, 176, 48, 252, 196, 100, 252, 216, 152, 8, 24, 88, 12, 36, 104, 20, 52, 124, 28, 68, 140, 40, 92, 164, 56, 120, 188, 72, 152, 216, 100, 172, 224, 92, 156, 52, 108, 176, 64, 124, 200, 76, 144, 224, 92, 224, 244, 252, 200, 236, 248, 180, 220, 236, 132, 188, 216, 88, 152, 172, 244, 0, 244, 245, 0, 245, 246, 0, 246, 247, 0, 247, 248, 0, 248, 249, 0, 249, 250, 0, 250, 251, 0, 251, 252, 0, 252, 253, 0, 253, 254, 0, 254, 255, 0, 255, 76, 24, 8, 108, 44, 24, 144, 72, 52, 176, 108, 84, 210, 146, 126, 252, 60, 0, 252, 84, 0, 252, 104, 0, 252, 124, 0, 252, 148, 0, 252, 172, 0, 252, 196, 0, 64, 0, 0, 255, 0, 0, 48, 48, 0, 64, 64, 0, 80, 80, 0, 255, 255, 0, 32, 68, 112, 36, 72, 116, 40, 76, 120, 44, 80, 124, 48, 84, 128, 72, 100, 144, 100, 132, 168, 216, 244, 252, 96, 128, 164, 68, 96, 140, 255, 255, 255)
SAFE_COLORS = set(range(1, 0xD7))
ALL_COLORS = set(range(256))
SPECTRA_PALETTE = {i:to_spectra(PALETTE[i * 3], PALETTE[i * 3 + 1], PALETTE[i * 3 + 2]) for i in range(256)}
WATER_COLORS = set(range(0xF5, 0xFF))
# ZOOM_OUT_4X, ZOOM_NORMAL, ZOOM_OUT_2X, ZOOM_OUT_8X, ZOOM_OUT_16X, ZOOM_OUT_32X = range(6)
ZOOM_4X, ZOOM_NORMAL, ZOOM_2X, ZOOM_8X, ZOOM_16X, ZOOM_32X = range(6)
BPP_8, BPP_32 = range(2)
def color_distance(c1, c2):
rmean = (c1.rgb[0] + c2.rgb[0]) / 2.
r = c1.rgb[0] - c2.rgb[0]
g = c1.rgb[1] - c2.rgb[1]
b = c1.rgb[2] - c2.rgb[2]
return math.sqrt(
((2 + rmean) * r * r) +
4 * g * g +
(3 - rmean) * b * b)
def find_best_color(x, in_range=SAFE_COLORS):
mj, md = 0, 1e100
for j in in_range:
c = SPECTRA_PALETTE[j]
d = color_distance(x, c)
if d < md:
mj, md = j, d
return mj
# def map_rgb_image(self, im):
# assert im.mode == 'RGB', im.mode
# data = np.array(im)
class BaseSprite:
def get_data(self):
raise NotImplemented
def get_data_size(self):
raise NotImplemented
class PaletteRemap(BaseSprite):
def __init__(self, ranges=None):
self.remap = np.arange(256, dtype=np.uint8)
if ranges:
self.set_ranges(ranges)
def get_data(self):
return b'\x00' + self.remap.tobytes()
def get_data_size(self):
return 257
@classmethod
def from_function(cls, color_func, remap_water=False):
res = cls()
for i in SAFE_COLORS:
res.remap[i] = find_best_color(color_func(SPECTRA_PALETTE[i]))
if remap_water:
for i in WATER_COLORS:
res.remap[i] = find_best_color(color_func(SPECTRA_PALETTE[i]))
return res
def set_ranges(self, ranges):
for r in ranges:
f, t, v = r
self.remap[f: t + 1] = v
def remap_image(self, im):
assert im.mode == 'P', im.mode
data = np.array(im)
data = self.remap[data]
res = Image.fromarray(data)
res.putpalette(PALETTE)
return res
class RealSprite(BaseSprite):
def __init__(self, w, h, *, xofs=0, yofs=0, zoom=ZOOM_4X):
self.sprite_id = None
self.w = w
self.h = h
# self.file = None
# self.x = None
# self.y = None
self.xofs = xofs
self.yofs = yofs
self.zoom = zoom
def get_data_size(self):
return 4
def get_data(self):
return struct.pack('<I', self.sprite_id)
def get_real_data(self):
raise NotImplementedError
def draw(self, img):
raise NotImplementedError
class ImageFile:
def __init__(self, filename, bpp=BPP_8):
assert(bpp == BPP_8) # TODO
self.filename = filename
self.bpp = bpp
self._image = None
def get_image(self):
if self._image:
return self._image
img = Image.open(self.filename)
assert (img.mode == 'P') # TODO
pal = tuple(img.getpalette())
if pal != PALETTE:
print(f'Custom palette in file {self.filename}, converting...')
# for i in range(256):
# if tuple(pal[i * 3: i*3 + 3]) != PALETTE[i * 3: i*3 + 3]:
# print(i, pal[i * 3: i*3 + 3], PALETTE[i * 3: i*3 + 3])
remap = PaletteRemap()
for i in ALL_COLORS:
remap.remap[i] = find_best_color(to_spectra(pal[3 * i], pal[3 * i + 1], pal[3 * i + 2]), in_range=ALL_COLORS)
self._image = remap.remap_image(img)
else:
self._image = img
return self._image
class FileSprite(RealSprite):
def __init__(self, file, x, y, w, h, **kw):
assert(isinstance(file, ImageFile))
super().__init__(w, h, **kw)
self.file = file
self.x = x
self.y = y
def get_real_data(self):
img = self.file.get_image()
img = img.crop((self.x, self.y, self.x + self.w, self.y + self.h))
raw_data = img.tobytes()
se = SpriteEncoder(True, False, None)
data = se.sprite_compress(raw_data)
return struct.pack(
'<IIBBHHhh',
self.sprite_id,
len(data) + 10, 0x04,
self.zoom,
self.h,
self.w,
self.xofs,
self.yofs,
) + data
# return struct.pack(
# '<IIBBHHhhI',
# self.sprite_id,
# len(data) + 14, 0x0C,
# self.zoom,
# self.h,
# self.w,
# self.xofs,
# self.yofs,
# len(raw_data),
# ) + data
class SpriteSheet:
def __init__(self, sprites=None):
self._sprites = list(sprites) if sprites else []
def make_image(self, filename, padding=5, columns=10):
w, h = 0, padding
lineofs = []
for i in range(0, len(self._sprites), columns):
w = max(w, sum(s.w for s in self._sprites[i: i + columns]))
lineofs.append(h)
h += padding + max((s.h for s in self._sprites[i: i + columns]), default=0)
w += (columns + 1) * padding
im = Image.new('L', (w, h), color=0xff)
im.putpalette(PALETTE)
x = 0
for i, s in enumerate(self._sprites):
y = lineofs[i // columns]
if i % columns == 0:
x = padding
s.x = x
s.y = y
s.file = filename
s.draw(im)
x += s.w + padding
im.save(filename)
def write_nml(self, file):
for s in self._sprites:
file.write(s.get_nml())
file.write('\n')
file.write('\n')
class DummySprite(BaseSprite):
def get_data(self):
return b'\x00'
def get_data_size(self):
return 1
class DescriptionSprite(BaseSprite): # action 8
def __init__(self, grfid, name, description):
assert isinstance(grfid, bytes)
assert isinstance(name, str)
assert isinstance(description, str)
self.grfid = grfid
self.name = name
self.description = description
self._data = b'\x08\x08' + self.grfid + self.name.encode('utf-8') + b'\x00' + self.description.encode('utf-8') + b'\x00'
def get_data(self):
return self._data
def get_data_size(self):
return len(self._data)
class InformationSprite(BaseSprite): # action 14
def __init__(self, palette): # TODO everything else
# self.palette = {'D': b'\x00', 'W': b'\x01', 'A': b'\x02'}[palette]
self.palette = palette.encode('utf-8')
self._data = b'\x14CINFOBPALS\x01\x00' + self.palette + b'\x00\x00'
def get_data(self):
return self._data
def get_data_size(self):
return len(self._data)
class ReplaceSprites: # action A
def __init__(self, sets):
assert isinstance(sets, (list, tuple))
assert len(sets) <= 0xff
for first, num in sets:
assert isinstance(first, int)
assert isinstance(num, int)
self.sets = sets
def get_data(self):
return bytes((0xa, len(self.sets))) + b''.join(struct.pack('<BH', num, first) for first, num in self.sets)
def get_data_size(self):
return 2 + 3 * len(self.sets)
class ReplaceNewSprites: # action 5
def __init__(self, set_type, num): # TODO offset
assert isinstance(set_type, int)
assert isinstance(num, int)
self.set_type = set_type
self.num = num
def get_data(self):
return bytes((0x5,)) + struct.pack('<BBH', self.set_type, 0xff, self.num)
def get_data_size(self):
return 5
class NewGRF:
def __init__(self, grfid, name, description):
self.sprites = []
self.pseudo_sprites = []
self.pseudo_sprites.append(InformationSprite('D'))
self.pseudo_sprites.append(DescriptionSprite(grfid, name, description))
self._next_sprite_id = 1
def add_sprite(self, *sprites):
assert(len(sprites) > 0)
if isinstance(sprites[0], RealSprite):
assert(all(isinstance(s, RealSprite) for s in sprites))
assert(len(set(s.zoom for s in sprites)) == len(sprites))
for s in sprites:
s.sprite_id = self._next_sprite_id
self._next_sprite_id += 1
for s in sprites:
self.sprites.append(s)
self.pseudo_sprites.append(sprites[0])
else:
assert(len(sprites) == 1)
self.pseudo_sprites.append(sprites[0])
def _write_pseudo_sprite(self, f, data, grf_type=0xff):
f.write(struct.pack('<IB', len(data), grf_type))
f.write(data)
def write(self, filename):
data_offset = 14
for s in self.pseudo_sprites:
data_offset += s.get_data_size() + 5
with open(filename, 'wb') as f:
f.write(b'\x00\x00GRF\x82\x0d\x0a\x1a\x0a') # file header
f.write(struct.pack('<I', data_offset))
f.write(b'\x00') # compression(1)
# f.write(b'\x04\x00\x00\x00') # num(4)
# f.write(b'\xFF') # grf_type(1)
# f.write(b'\xb0\x01\x00\x00') # num + 0xff -> recoloursprites() (257 each)
self._write_pseudo_sprite(f, b'\x02\x00\x00\x00')
for s in self.pseudo_sprites:
self._write_pseudo_sprite(f, s.get_data(), grf_type=0xfd if isinstance(s, RealSprite) else 0xff)
f.write(b'\x00\x00\x00\x00')
for s in self.sprites:
f.write(s.get_real_data())
f.write(b'\x00\x00\x00\x00')

View File

@@ -0,0 +1,8 @@
##grflangid 0x01
STR_GRF_NAME :CityMania Alpine Landscape
STR_GRF_DESCRIPTION :Modified OpenGFX spritess for alpine climate
STR_TEST_OBJECT : CM Test object
STR_FLMA : Flower Meadow
STR_TREE : Tree
STR_CREEK : Creek
STR_MINEFIELD : Minefield

560
grf/alpine/readgrftest.py Normal file
View File

@@ -0,0 +1,560 @@
import sys
import struct
from nml import lz77
def hex_str(s):
if isinstance(s, (bytes, memoryview)):
return ':'.join('{:02x}'.format(b) for b in s)
return ':'.join('{:02x}'.format(ord(c)) for c in s)
def read_extended_byte(data, offset):
res = data[offset]
if res != 0xff:
return res, offset + 1
return data[offset + 1] | (data[offset + 2] << 8), offset + 3
def read_dword(data, offset):
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24), offset + 4
FEATURES = {
0: 'Train',
1: 'RV',
2: 'Ship',
3: 'Aircraft',
4: 'Station',
5: 'Canal',
6: 'Bridge',
7: 'House',
8: 'Setting',
9: 'IndTiles',
0xa: 'Industry',
0xb: 'Cargo',
0xc: 'Sound',
0xd: 'Airport',
0xe: '?Signals?',
0xf: 'Object',
0x10: 'Railtype',
0x11: 'AirportTiles',
0x12: 'Roadtype',
0x13: 'Tramtype',
}
ACTION0_TRAIN_PROPS = {
0x05: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Track type (see below) should be same as front
0x08: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 AI special flag: set to 1 if engine is 'optimized' for passenger service (AI won't use it for other cargo), 0 otherwise no
0x09: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Speed in mph*1.6 (see below) no
0x0B: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Power (0 for wagons) should be zero
0x0D: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Running cost factor (0 for wagons) should be zero
0x0E: 'D', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Running cost base, see below should be zero
0x12: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Sprite ID (FD for new graphics) yes
0x13: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Dual-headed flag; 1 if dual-headed engine, 0 otherwise should be zero also for front
0x14: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Cargo capacity yes
0x15: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Cargo type, see CargoTypes
0x16: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Weight in tons should be zero
0x17: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Cost factor should be zero
0x18: 'B', # Supported by OpenTTD <0.7<0.7 Supported by TTDPatch 2.02.0[1] Engine rank for the AI (AI selects the highest-rank engine of those it can buy) no
0x19: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 GRFv≥1 Engine traction type (see below) no
0x1A: 'B*', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 GRFv≥1 Not a property, but an action: sort the purchase list. no
0x1B: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Power added by each wagon connected to this engine, see below should be zero
0x1C: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Refit cost, using 50% of the purchase price cost base yes
0x1D: 'D', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Bit mask of cargo types available for refitting, see column 2 (bit value) in CargoTypes yes
0x1E: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Callback flags bit mask, see below yes
0x1F: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 19)2.5 Coefficient of tractive effort should be zero
0x20: 'B', # Supported by OpenTTD 1.11.1 Supported by TTDPatch 2.5 (alpha 27)2.5 Coefficient of air drag should be zero
0x21: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 GRFv≥2 Make vehicle shorter by this amount, see below yes
0x22: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Set visual effect type (steam/smoke/sparks) as well as position, see below yes
0x23: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Set how much weight is added by making wagons powered (i.e. weight of engine), see below should be zero
0x24: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 44)2.5 High byte of vehicle weight, weight will be prop.24*256+prop.16 should be zero
0x25: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 44)2.5 User-defined bit mask to set when checking veh. var. 42 yes
0x26: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 44)2.5 Retire vehicle early, this many years before the end of phase 2 (see Action0General) no
0x27: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 58)2.5 Miscellaneous flags partly
0x28: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 58)2.5 Refittable cargo classes yes
0x29: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 58)2.5 Non-refittable cargo classes yes
0x2A: 'D', # Supported by OpenTTD 0.6 (r7191)0.6 Supported by TTDPatch 2.5 (r1210)2.5 Long format introduction date no
0x2B: 'W', # Supported by OpenTTD 1.2 (r22713)1.2 Not supported by TTDPatch Custom cargo ageing period yes
0x2C: 'n*B', # Supported by OpenTTD 1.2 (r23291)1.2 Not supported by TTDPatch List of always refittable cargo types yes
0x2D: 'n*B', # Supported by OpenTTD 1.2 (r23291)1.2 Not supported by TTDPatch List of never refittable cargo types yes
0x2E: 'W', # Supported by OpenTTD 12 (g2183fd4dab)12 Not supported by TTDPatch Maximum curve speed modifier yes
}
ACTION0_OBJECT_PROPS = {
0x08: ('label', 'L'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Class label, see below
0x09: ('class_name_id', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Text ID for class
0x0A: ('name_id', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Text ID for this object
0x0B: ('climate', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Climate availability
0x0C: ('size', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Byte representing size, see below
0x0D: ('build_cost_factor', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Object build cost factor (sets object removal cost factor as well)
0x0E: ('intro_date', 'D'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Introduction date, see below
0x0F: ('eol_date', 'D'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 End of life date, see below
0x10: ('flags', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Object flags, see below
0x11: ('anim_info', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Animation information
0x12: ('anim_speed', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Animation speed
0x13: ('anim_trigger', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Animation triggers
0x14: ('removal_cost_factor', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Object removal cost factor (set after object build cost factor)
0x15: ('cb_flags', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Callback flags, see below
0x16: ('building_height', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Height of the building
0x17: ('num_views', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Number of object views
0x18: ('num_objects', 'B'), # Supported by OpenTTD 1.4 (r25879)1.4 Not supported by TTDPatch Measure for number of objects placed upon map creation
}
ACTION0_PROPS = {
0: ACTION0_TRAIN_PROPS,
0xf: ACTION0_OBJECT_PROPS,
}
def str_feature(feature):
return f'{FEATURES[feature]}<{feature:02x}>'
def str_sprite(sprite):
sprite_id = sprite & 0x1fff
draw = {0: 'N', 1 : 'T', 2: 'R'}[(sprite >> 14) & 3]
color_translation = (sprite >> 16) & 0x3fff
normal_in_transparent = bool(sprite & (1 << 30))
sprite_type = sprite >> 31
ntstr = ['', ' NF'][normal_in_transparent]
return f'[{sprite_id} {draw}{ntstr} {color_translation}-{sprite_type}]'
def read_property(data, ofs, fmt):
if fmt == 'B':
return data[ofs], ofs + 1
if fmt == 'W':
return data[ofs] | (data[ofs + 1] << 8), ofs + 2
if fmt == 'L':
return data[ofs: ofs + 4], ofs + 4
if fmt == 'D':
return read_dword(data, ofs)
if fmt == 'B*':
return read_extended_byte(data, ofs)
if fmt == 'n*B':
n = data[ofs]
return data[ofs + 1: ofs + 1 + n], ofs + 1 + n
assert False, fmt
def decode_action0(data):
num = data[0]
feature = data[0]
num_props = data[1]
num_info = data[2]
first_id, ofs = read_extended_byte(data, 3)
props = {}
for _ in range(num_props):
prop = data[ofs]
propdict = ACTION0_PROPS[feature]
name, fmt = propdict[prop]
value, ofs = read_property(data, ofs + 1, fmt)
key = f'{name}<{prop:02x}>'
assert key not in props, key
props[key] = value
print(f' <0>FEATURE feature:{str_feature(feature)} num_info:{num_info} first_id:{first_id} props:{props}')
def decode_action1(data):
num = data[0]
feature = data[0]
num_sets = data[1]
num_ent, _ = read_extended_byte(data, 2)
print(f' <1>SPRITESET feature:{str_feature(feature)} num_sets:{num_sets} num_ent:{num_ent}')
SPRITE_GROUP_OP = [
'ADD', # a + b
'SUB', # a - b
'SMIN', # (signed) min(a, b)
'SMAX', # (signed) max(a, b)
'UMIN', # (unsigned) min(a, b)
'UMAX', # (unsigned) max(a, b)
'SDIV', # (signed) a / b
'SMOD', # (signed) a % b
'UDIV', # (unsigned) a / b
'UMOD', # (unsigned) a & b
'MUL', # a * b
'AND', # a & b
'OR', # a | b
'XOR', # a ^ b
'STO', # store a into temporary storage, indexed by b. return a
'RST', # return b
'STOP', # store a into persistent storage, indexed by b, return a
'ROR', # rotate a b positions to the right
'SCMP', # (signed) comparison (a < b -> 0, a == b = 1, a > b = 2)
'UCMP', # (unsigned) comparison (a < b -> 0, a == b = 1, a > b = 2)
'SHL', # a << b
'SHR', # (unsigned) a >> b
'SAR', # (signed) a >> b
]
class DataReader:
def __init__(self, data, offset=0):
self.data = data
self.offset = offset
def get_byte(self):
self.offset += 1
return self.data[self.offset - 1]
def get_extended_byte(self):
res, self.offset = read_extended_byte(self.data, self.offset)
return res
def get_word(self):
return self.get_byte() | (self.get_byte() << 8)
def get_var(self, n):
size = 1 << n
res = struct.unpack_from({0: '<B', 1: '<H', 2: '<I'}[n], self.data, offset=self.offset)[0]
self.offset += size
return res
TLF_NOTHING = 0x00
TLF_DODRAW = 0x01 # Only draw sprite if value of register TileLayoutRegisters::dodraw is non-zero.
TLF_SPRITE = 0x02 # Add signed offset to sprite from register TileLayoutRegisters::sprite.
TLF_PALETTE = 0x04 # Add signed offset to palette from register TileLayoutRegisters::palette.
TLF_CUSTOM_PALETTE = 0x08 # Palette is from Action 1 (moved to SPRITE_MODIFIER_CUSTOM_SPRITE in palette during loading).
TLF_BB_XY_OFFSET = 0x10 # Add signed offset to bounding box X and Y positions from register TileLayoutRegisters::delta.parent[0..1].
TLF_BB_Z_OFFSET = 0x20 # Add signed offset to bounding box Z positions from register TileLayoutRegisters::delta.parent[2].
TLF_CHILD_X_OFFSET = 0x10 # Add signed offset to child sprite X positions from register TileLayoutRegisters::delta.child[0].
TLF_CHILD_Y_OFFSET = 0x20 # Add signed offset to child sprite Y positions from register TileLayoutRegisters::delta.child[1].
TLF_SPRITE_VAR10 = 0x40 # Resolve sprite with a specific value in variable 10.
TLF_PALETTE_VAR10 = 0x80 # Resolve palette with a specific value in variable 10.
TLF_KNOWN_FLAGS = 0xFF # Known flags. Any unknown set flag will disable the GRF.
# /** Flags which are still required after loading the GRF. */
TLF_DRAWING_FLAGS = ~TLF_CUSTOM_PALETTE
# /** Flags which do not work for the (first) ground sprite. */
TLF_NON_GROUND_FLAGS = TLF_BB_XY_OFFSET | TLF_BB_Z_OFFSET | TLF_CHILD_X_OFFSET | TLF_CHILD_Y_OFFSET
# /** Flags which refer to using multiple action-1-2-3 chains. */
TLF_VAR10_FLAGS = TLF_SPRITE_VAR10 | TLF_PALETTE_VAR10
# /** Flags which require resolving the action-1-2-3 chain for the sprite, even if it is no action-1 sprite. */
TLF_SPRITE_REG_FLAGS = TLF_DODRAW | TLF_SPRITE | TLF_BB_XY_OFFSET | TLF_BB_Z_OFFSET | TLF_CHILD_X_OFFSET | TLF_CHILD_Y_OFFSET
# /** Flags which require resolving the action-1-2-3 chain for the palette, even if it is no action-1 palette. */
TLF_PALETTE_REG_FLAGS = TLF_PALETTE
def read_sprite_layout_registers(d, flags, is_parent):
regs = {'flags': flags & TLF_DRAWING_FLAGS}
if flags & TLF_DODRAW: regs['dodraw'] = d.get_byte();
if flags & TLF_SPRITE: regs['sprite'] = d.get_byte();
if flags & TLF_PALETTE: regs['palette'] = d.get_byte();
if is_parent:
delta = [d.get_byte(), d.get_byte(), 0] if flags & TLF_BB_XY_OFFSET else [0, 0, 0]
if flags & TLF_BB_Z_OFFSET: delta[2] = d.get_byte()
regs['delta_parent'] = tuple(delta)
else:
delta, delta_set = [0, 0], False
if flags & TLF_CHILD_X_OFFSET: delta[0], delta_set = d.get_byte(), True
if flags & TLF_CHILD_Y_OFFSET: delta[1], delta_set = d.get_byte(), True
if delta_set: regs['delta_child'] = tuple(delta)
if flags & TLF_SPRITE_VAR10: regs['sprite_var10'] = d.get_byte()
if flags & TLF_PALETTE_VAR10: regs['palette_var10'] = d.get_byte()
return regs
def read_sprite_layout(d, num, no_z_position):
has_z_position = not no_z_position
has_flags = bool((num >> 6) & 1)
num &= 0x3f
def read_sprite():
sprite = d.get_word()
pal = d.get_word()
flags = d.get_word() if has_flags else TLF_NOTHING
return {'sprite': sprite, 'pal': pal, 'flags': flags}
ground = read_sprite()
ground_regs = read_sprite_layout_registers(d, ground['flags'], False)
sprites = []
for _ in range(num):
seq = {}
seq['sprite'] = read_sprite()
delta = seq['delta'] = (d.get_byte(), d.get_byte(), d.get_byte() if has_z_position else 0)
is_parent = (delta[2] != 0x80)
if is_parent:
seq['size'] = (d.get_byte(), d.get_byte(), d.get_byte())
seq['regs'] = read_sprite_layout_registers(d, seq['sprite']['flags'], is_parent)
sprites.append(seq)
return {
'ground': {
'sprite': ground,
'regs': ground_regs,
},
'sprites': sprites
}
def decode_action2(data):
feature = data[0]
set_id = data[1]
num_ent1 = data[2]
d = DataReader(data, 3)
print(f' <2>SPRITEGROUP feature:{str_feature(feature)} set_id:{set_id} ', end='')
if feature in (0x07, 0x09, 0x0f, 0x11):
if num_ent1 == 0:
ground_sprite, building_sprite, xofs, yofs, xext, yext, zext = struct.unpack_from('<IIBBBBB', data, offset=3)
ground_sprite = str_sprite(ground_sprite)
building_sprite = str_sprite(building_sprite)
print(f'ground_sprite:{ground_sprite} building_sprite:{building_sprite} '
f'xofs:{xofs} yofs:{yofs} extent:({xext}, {yext}, {zext})')
return
if num_ent1 < 0x3f:
raise NotImplemented
if num_ent1 in (0x81, 0x82, 0x85, 0x86, 0x89, 0x8a):
group_size = (num_ent1 >> 2) & 3
first = True
ofs = 3
adjusts = []
while True:
res = {}
res['op'] = 0 if first else d.get_byte()
var = res['var'] = d.get_byte()
if var == 0x7e:
res['subroutine'] = d.get_byte()
else:
res['parameter'] = d.get_byte() if 0x60 <= var < 0x80 else 0
varadj = d.get_byte()
res['shift_num'] = varadj & 0x1f
has_more = bool(varadj & 0x20)
res['type'] = varadj >> 6
res['and_mask'] = d.get_var(group_size)
if res['type'] != 0:
res['add_val'] = d.get_var(group_size)
res['divmod_val'] = d.get_var(group_size)
adjusts.append(res)
if not has_more:
break
n_ranges = d.get_byte()
ranges = []
for _ in range(n_ranges):
group = d.get_word()
low = d.get_var(group_size)
high = d.get_var(group_size)
ranges.append((group, low, high))
default_group = d.get_word()
print(f'default_group: {default_group} adjusts:{adjusts} ranges:{ranges} ')
return
layout = read_sprite_layout(d, max(num_ent1, 1), num_ent1 == 0)
print(f'layout:{layout}')
return
# num_loaded = num_ent1
# num_loading = get_byte()
# [get_word() for i in range(num_loaded)]
# [get_word() for i in range(num_loading)]
# assert False, num_ent1
# # assert num_ent1 < 0x3f + 0x40, num_ent1
# return
num_ent2 = data[3]
ent1 = struct.unpack_from('<' + 'H' * num_ent1, data, offset=4)
ent2 = struct.unpack_from('<' + 'H' * num_ent2, data, offset=4 + 2 * num_ent1)
print(f'ent1:{ent1} ent2:{ent2}')
def decode_action4(data):
fmt = '<BBB' + ('H' if data[1] & 0xf0 else 'B')
feature, lang, num, offset = struct.unpack_from(fmt, data)
strings = [s.decode('utf-8') for s in data[struct.calcsize(fmt):].split(b'\0')[:-1]]
print(f' <4>STRINGS feature:{str_feature(feature)} lang:{lang} num:{num} offset:{offset} strings:{strings}')
def decode_action5(data):
t = data[0]
offset = 0
num, dataofs = read_extended_byte(data, 1)
if t & 0xf0:
offset, _ = read_extended_byte(data, dataofs)
t &= ~0xf0
print(f' <5>REPLACENEW type:{t} num:{num}, offset:{offset}')
def decode_action6(data):
d = DataReader(data, 0)
params = []
while True:
param_num = d.get_byte()
if param_num == 0xFF:
break
param_size = d.get_byte()
offset = d.get_extended_byte()
params.append({'num': param_num, 'size': param_size, 'offset': offset})
print(f' <6>EDITPARAM params:{params}')
def decode_actionA(data):
num = data[0]
sets = [struct.unpack_from('<BH', data, offset=3*i + 1) for i in range(num)]
print(f' <A>REPLACEBASE sets:<{num}>{sets}')
OPERATIONS = {
0x00: '{target} = {source1}', # Supported by OpenTTD Supported by TTDPatch Assignment target = source1
0x01: '{target} = {source1} + {source2}', # Supported by OpenTTD Supported by TTDPatch Addition target = source1 + source2
0x02: '{target} = {source1} - {source2}', # Supported by OpenTTD Supported by TTDPatch Subtraction target = source1 - source2
0x03: '{target} = {source1} * {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch Unsigned multiplication target = source1 * source2, with both sources being considered to be unsigned
0x04: '{target} = {source1} * {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch Signed multiplication target = source1 * source2, with both sources considered signed
0x05: '{target} = {source1} <</>> {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch Unsigned bit shift target = source1 << source2 if source2>0, or target = source1 >> abs(source2) if source2 < 0. source1 is considered to be unsigned
0x06: '{target} = {source1} <</>> {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch Signed bit shift same as 05, but source1 is considered signed)
0x07: '{target} = {source1} & {source2}', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 48)2.5 Bitwise AND target = source1 AND source2
0x08: '{target} = {source1} | {source2}', # Suported by OpenTTD Supported by TTDPatch 2.5 (alpha 48)2.5 Bitwise OR target = source1 OR source2
0x09: '{target} = {source1} / {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Unsigned division target = source1 / source2
0x0A: '{target} = {source1} / {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Signed division target = source1 / source2
0x0B: '{target} = {source1} % {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Unsigned modulo target = source1 % source2
0x0C: '{target} = {source1} % {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Signed modulo target = source1 % source2
}
def decode_actionD(data):
target = data[0]
operation = data[1]
source1 = data[2]
source2 = data[3]
if source1 == 0xff or source2 == 0xff:
value, _ = read_dword(data, 4)
fmt = OPERATIONS[operation]
sf = lambda x: f'[{x:02x}]' if x != 0xff else str(value)
target_str = f'[{target:02x}]'
op_str = fmt.format(target=target_str, source1=sf(source1), source2=sf(source2))
print(f' <A>OP {op_str}')
def decode_action14(data):
res = {}
ofs = 0
def decode_chunk(res):
nonlocal ofs
chunk_type = data[ofs]
ofs += 1
if chunk_type == 0: return False
chunk_id = data[ofs: ofs + 4]
ofs += 4
if chunk_type == b'C'[0]:
res[chunk_id] = {}
while decode_chunk(res[chunk_id]):
pass
elif chunk_type == b'B'[0]:
l = data[ofs] | (data[ofs + 1] << 8)
res[chunk_id] = data[ofs + 2: ofs + 2 + l]
ofs += 2 + l
# elif chunk_type == b'T'[0]:
else:
assert False, chunk_type
return True
while decode_chunk(res):
pass
print(f' <14>INFO {res}')
ACTIONS = {
0x00: decode_action0,
0x01: decode_action1,
0x02: decode_action2,
0x04: decode_action4,
0x05: decode_action5,
0x06: decode_action6,
0x0a: decode_actionA,
0x0d: decode_actionD,
0x14: decode_action14,
}
def read_pseudo_sprite(f):
l = struct.unpack('<I', f.read(4))[0]
if l == 0:
print('End of pseudo sprites')
return False
grf_type = f.read(1)[0]
grf_type_str = hex(grf_type)[2:]
data = f.read(l)
print(f'Sprite({l}, {grf_type_str}): ', hex_str(data[:100]))
if grf_type == 0xff:
decoder = ACTIONS.get(data[0])
if decoder:
decoder(data[1:])
return True
def decode_sprite(f, num):
data = b''
while num > 0:
code = f.read(1)[0]
if code >= 128: code -= 256
# print(f'Code {code} num {num}')
if code >= 0:
size = 0x80 if code == 0 else code
num -= size
if num < 0: raise RuntimeError('Corrupt sprite')
data += f.read(size)
else:
data_offset = ((code & 7) << 8) | f.read(1)[0]
#if (dest - data_offset < dest_orig.get()) return WarnCorruptSprite(file, file_pos, __LINE__);
size = -(code >> 3)
num -= size
if num < 0: raise RuntimeError('Corrupt sprite')
data += data[-data_offset:size - data_offset]
if num != 0: raise RuntimeError('Corrupt sprite')
return data
def read_real_sprite(f):
sprite_id = struct.unpack('<I', f.read(4))[0]
if sprite_id == 0:
print(f'End of real sprites')
return False
print(f'Real sprite({sprite_id}): ', end='')
num, t = struct.unpack('<IB', f.read(5))
start_pos = f.tell()
print(f'({num}, {t:02x}): ', end='')
if t == 0xff:
print('non-real (skip)')
f.seek(start_pos + num - 1, 0)
return True
zoom, height, width, x_offs, y_offs = struct.unpack('<BHHhh', f.read(9))
bpp = 1 # TODO
decomp_size = struct.unpack('<I', f.read(4))[0] if t & 0x08 else width * height * bpp
print(f'{width}x{height} zoom={zoom} x_offs={x_offs} y_offs={y_offs} bpp={bpp} decomp_size={decomp_size}')
# data = decode_sprite(f, decomp_size)
# print('Data: ', hex_str(data[:40]))
f.seek(start_pos + num - 1, 0)
return True
with open(sys.argv[1], 'rb') as f:
print('Header:', hex_str(f.read(10)))
data_offest, compression = struct.unpack('<IB', f.read(5))
header_offset = f.tell() - 1
print(f'Offset: {data_offest} compresion: {compression}')
while read_pseudo_sprite(f):
pass
real_data_offset = f.tell() - header_offset
# while read_real_sprite(f):
# pass
if data_offest != real_data_offset:
print(f'[ERROR] Data offset check failed: {data_offest} {real_data_offset}')