Feature #2746

station support

Added by Hirundo over 8 years ago. Updated about 5 years ago.

Status:NewStart date:2011-06-16
Priority:NormalDue date:
Assignee:-% Done:

0%

Category:-
Target version:-

Description

Stations are a very hard feature to get implemented in NML. My suggestion was as follows:

  • All sprites in a single action1 / real action2
  • All stations automatically reference this action2
  • Deciding graphics is done via CB 14 (sprite layout). Using extended sprite layouts, it's possible to select the sprites as well.
  • All sprite layouts are added to the action0 of all stations, using property 0A (copy layout) if possible.

See also some example code
http://pastebin.com/7uH9pa8b

There are still a lot of issues to look at. To name a few:
  • Moving spritelayouts around creates problems related to #2649, when these sprite layouts use parameters that change their value. A possible solution would be to allocate a temporary parameter.
  • Action3 and real action2 become no-ops, their functionality needs to be achieved using varaction2. In the case of action3, this requires a new variable 'cargo type with highest amount' and var60.
  • Station layouts (0E) are really cumbersome because you need to define them for all possible sizes. I suggest we don't even bother with it. Selecting graphics is better done via var40 and friends. The 'tile type' concept is only really needed for properties 11, 14 (caternary) and 15 (track/non-track). IMO, we'd best do that via CB 24.
  • Foundation sprites also need some thought.
  • Mind not to forget explicit support for the purchase menu.

#1555 would help a lot for stations. I'd almost consider it a prerequisite, as we'd better do it right the first time.

History

#1 Updated by yexo over 8 years ago

Counter-proposal:

Keep the same syntax for spritelayout as for other features with only one difference: the "sprite" property doesn't get a reference to a spriteset as value but instead a constant number that is an index in a spriteset. The actual spriteset is selected via switch-blocks. Code will look like this;

spriteset sprites1 {
[....,"groundsprite1.png"]
[....,"building1A.png"]
[....,"building1B.png"]
}
spriteset sprites2 {
[....,"groundsprite2.png"]
[....,"building2A.png"]
[....,"building2B.png"]
}
spritelayout station_layout {
ground { sprite: 0; }
building { sprite: 1; }
building { sprite: 2; }
}
switch(FEAT_STATIONS, station_chose_graphics, ....){
1: sprites1;
sprites2;
}
switch(FEAT_STATIONS, station_cb_switch, current_callback){
0x14: return 0; // We need a way to make this prettier. Should return an index in the layouts property defined below
station_chose_graphics;
}
item(FEAT_STATIONS, ...) {
property { layouts: [station_layout]; }
graphics { station_cb_switch; }
}

This means we stay closer to the NFO code. Disadvantage is obviously that this results in somewhat harder to understand NML code. Advantages are the relative ease (I think) to implement this and keeping a lot of possibilities open. When using advanced sprite layouts every sprite in the spritelayout gets an optional new property "extra_cb_info1_value" (needs a better name) which you can set to 0..7. If set, the value of that will be stored in extra_callback_info1 which can be used in the station_chose_graphics switch-block to chose a different spriteset per building.

#2 Updated by Hirundo over 8 years ago

What if sprites1, sprites2, .. spritesN share a common ground sprite? How would you code that in NML?

As discussed, I'm working on removing 'ttdsprite' and letting 'sprite' refer to both an action1 (spriteset ref) or ttd sprite (number). The same would happen for 'palette', implementing ESL bit 3. How would 'sprite' distinguish between a TTD sprite and an index from action1? Perhaps use a different name (sprite_index?) for this?

#3 Updated by yexo over 8 years ago

Yes, this means that we best don't merge "sprite" and "ttdsprite" to keep a distinction between them.

spriteset groundset {
[....,"groundsprite.png"]
}
spriteset sprites1 {
[....,"building1A.png"]
[....,"building1B.png"]
}
spriteset sprites2 {
[....,"building2A.png"]
[....,"building2B.png"]
}
spritelayout station_layout {
ground { sprite: 0; } // ground sprite has implied "extra_cb_info1_value: 0:"
building { sprite: 0; } // All other sprites have implied "extra_cb_info1_value: 1"
building { sprite: 1; }
}
switch(FEAT_STATIONS, chose_building_sprites, ...){
0: sprites1;
sprites2;
}
switch(FEAT_STATIONS, station_chose_graphics, extra_callback_info1){
0: groundset;
chose_building_sprites;
}
switch(FEAT_STATIONS, station_cb_switch, current_callback){
0x14: return 0; // We need a way to make this prettier. Should return an index in the layouts property defined below
station_chose_graphics;
}

#4 Updated by Hirundo over 8 years ago

Same example, coded differently

spriteset groundset {
  [....,"groundsprite.png"]
}
// notice that sprites are grouped like construction stages
spriteset sprites1 {
  [....,"building1A.png"]
  [....,"building2A.png"]
}
spriteset sprites2 {
  [....,"building1B.png"]
  [....,"building2B.png"]
}
spritelayout station_layout(gen) {
  ground { sprite: groundset; }
  building { sprite: sprites1(gen); } 
  building { sprite: sprites2(gen); }
}
// Note that in principle, there would be no objections to passing sprite sets as parameters.
// Since all parameter combinations are known at compile time, we can always determine what combinations are used.
// For stations, use of var10 / ASL bit 6/7 might make the process more efficient.
switch(FEAT_STATIONS, station_switch_layout, ...){
  0: station_layout(0);
  station_layout(1);
}

switch(FEAT_STATIONS, station_cb_switch, current_callback){
  0x14: station_switch_layout;
  CB_FAILED;
}

Edit: see also http://pastebin.com/Zvny0Eke (added for later reference)

#5 Updated by frosch about 5 years ago

New syntax suggestion, that tries to stay very close to industries/objects. (credits: Elyon and me)

Input NML:

spriteset(stationset1, "src/gfx/cc_grid.png") { tmpl_groundsprites(1, 1) }

spritegroup spritegroup1 { // alternatively: reference spriteset directly in spritelayout, and generate a spritegroup automatically using the same spriteset for all cases
  little: [ ];
  lots:   [ stationset1 ];
}

spriteset(stationset2, "src/gfx/cc_build.png") { tmpl_buildingsprites(1, 1) }
spriteset(stationset3, "src/gfx/cc_build.png") { tmpl_buildingsprites(1, 2) }
spriteset(stationset4, "src/gfx/cc_build.png") { tmpl_buildingsprites(1, 3) }

spritegroup spritegroup2 {
  little: [ stationset2 ];
  lots:   [ stationset3, stationset4 ];
}

spritelayout stationlayout2 {
  ground {
    sprite: spritegroup1(0)
    orientation_offset: 0 // for now a compile-time constant, added to the sprite for Y direction. in this case use sprite 0 for both orientations
  }
  building {
    sprite: spritegroup2(0)
    orientation_offset: 1 // use sprite 0 for X and 0+1 for Y
  }
}

spritelayout stationlayout3 {
  ground_track // build-in macro for "sprite: (var42 & 0x05) ? 1038 : 1012" and "orientation_offset: -1" 
  building {
    sprite: spritegroup2(0)
    orientation_offset: 1 // use sprite 0 for X and 0+1 for Y
  }
}

switch (FEAT_STATION, SELF, switch3, [ ... ] {
  ...
  stationlayout2;
}

switch (FEAT_STATION, SELF, switch4, [ ... ] {
  ...
  stationlayout3;
}

item(FEAT_STATION, station4) {
  property {
    ... nothing about layouts here ...
  }
  graphics {
    purchase: switch2
    COAL: switch2           // use COAL amount for little/lots
    unknown_cargo: switch2  // use 0 amount for little/lots
    default: switch2        // use sum amounts for little/lots
  }
}

Output GRF:

action0:
  layout0: stationlayout2 using var10=0 for ground, var10=1 for building1
  layout1: same as layout1, but
            - all X and Y values swapped
            - orientation_offset applied

action3
->  varactions according to nml-switches (switch3)
    -> varaction(var0C)
         14: return layout0
         default:
           -> varaction(var10)
              0: return stationgroup1
              1: return stationgroup2

Notes:
  • The action0 spritelayouts and callback14 are generated automatically by NML.
  • Each station spritelayout creates two action0 layout entries (for X and Y orientation)
    • In the GRF the spritelayouts are only defined for the first station (propery 1A)
    • Other stations copy the spritelayouts completely (property 0A)
    • So spritelayouts have the same id, no matter which item references them via which switches
    • The Y orientation spritelayout is generated from the same NML spritelayout using "orientation_offset" for the sprite.
    • Advanced: Detect identical spritelayouts
  • Each station spritelayout creates two varaction2 items
    • Check callback 14, and return the id of the spritelayout
    • Check var 10, and return the spritegroup for the spritelayout item.
  • Usage of var 10:
    • The spritelayout may reference different spritegroups.
    • The spritelayout assigns var10 ids 0-1, 3-7 to these. (2 is used for custom foundations, and not usable).
    • Using more than 7 different spritegroups in one spritelayout is invalid.
  • The graphics-chain for var10 and callback 14 use the same nml switches all the way, so temporary registers are assigned the same values in all var10 cases, i.e. for all spritelayouts, and can thus be used as in industry/object spritelayouts
Open issues:
  • "orientation_offset" is somewhat long
  • "unknown_cargo" and "default": only one makes sense at a time, so maybe this should be a property instead? "use_none_as_default"? or "default_amount: none", "default_amount: sum"?
  • Not only the sprites need different offsets in the Y layout, but possibly also the sprite position (esp. for child sprites)
    • Either multiple "orientation_offsets" like "sprite_orientation", "xoffset_orientation", ...
    • Or some type of pair for every layout value (xoffset: [0, 12]; yoffset: [12, 0]; xextent: [16, 4]; yextent: [4, 16]; zextent: 20). Though ideally the syntax should not suggest to use non-constant differences between the orientations, esp. not different spritegroups. While this is possible in theory (orientation is in var40 bit 24), it seems to be against the nature of stations, and may thus run into weird issues.

#6 Updated by frosch about 5 years ago

Update on the spritelayout syntax:

  spritelayout concrete_platform_layout {
    ground_track // build-in macro for "sprite: (var42 & 0x05) ? 1038 : 1012" and "index: [0, -1] + railtype-magic" 
    building {
      sprite: platform_set;
      index: [0, 1]
      yextent: 4;  // automatically extended to [4, 16], since 16 is the default for extent
    }
    childsprite {
      sprite: cargo_set;
      // index: 0; // default
      xoffset: [32, 16];
      yoffset: -16;
    }
    building {
      sprite: platform_set(2);
      index: [2, 3]
      yoffset: 12; // automatically extended to [12, 0]
      yextent: 4;  // automatically extended to [4, 16], since 16 is the default for extent
    }
  }

  • All items inside the spritelayout, except "sprite" and "palette" are pairs for X and Y orientation.
  • The Y orientation is added automatically, if omitted:
    • For "building" sprites, x/yoffset and x/yextent are swapped.
    • All other items are set identical.
  • "sprite" can reference a single "spritegroup" or "spriteset", without any parameters; or a global sprite number
  • "index" are the orientation-specific offsets/parameters to the references spriteset resp. to the spritesets in the spritegroup resp. to the global sprite nunber.
  • "sprite" must be a compile-time constant, all other items (including "index") do not need to be constant.
  • There is no (easy) syntax to enable the traditional "railtype" sprite offset, but noone would need it anyway, except: The build-in macro "ground_track" uses it.
Open issues:
  • "palette" is to be discussed later (it could be like "sprite" with a "palette_index")
  • "default_amount: none/sum", see previous post

#7 Updated by Elyon about 5 years ago

Elaborate example based on some of CATS' requirements:

spriteset(platforms, "graphics/platforms.png") {
  concrete: template_platforms(0);
  cobble: template_platforms(1);
  dirt: template_platforms(2);
}

spriteset(buildings, "graphics/buildings.png") {
  small: template_buildings(0);
  large: template_buildings(1);
}

spriteset(little_cargoes, "graphics/cargoes.png") {
  template_cargoes(0, 0);
  template_cargoes(0, 1);
  // ...
  template_cargoes(0, 46);
  template_cargoes(0, 47);
}

spriteset(lots_cargoes, "graphics/cargoes.png") {
  template_cargoes(1, 0);
  template_cargoes(1, 1);
  // ...
  template_cargoes(1, 46);
  template_cargoes(1, 47);
}

spriteset(signals, "graphics/signals.png") {
  red: template_signals(0);
  yellow: template_signals(1);
  green: template_signals(2);
}

spritegroup cargoes {
  little: [little_cargoes];
  lots: [lots_cargoes];
}

spritelayout concrete_platform {
  ground_track
  building {
    sprite: platforms(concrete + platform_variation);
    index: [0, 2];
    yextent: 4;
  }
  childsprite {
    sprite: buildings(small + building_variation);
    index: [0, 1];
  }
  childsprite {
    sprite: cargoes(cargo_type + cargo_size);
    yoffset: 4;
  }
  childsprite {
    sprite: cargoes(cargo_type + cargo_size);
    xoffset: 8;
    yoffset: 8;
  }
  childsprite {
    sprite: cargoes(cargo_type + cargo_size);
    xoffset: 16;
    yoffset: 12;
  }
  childsprite {
    sprite: cargoes(cargo_type + cargo_size);
    xoffset: 24;
    yoffset: 16;
  }
  childsprite {
    sprite: signals(red);
    index: [0, 1];
    xoffset: [0, 16];
    hide_sprite: !red_signal;
  }
  childsprite {
    sprite: signals(yellow);
    index: [0, 1];
    xoffset: [0, 16];
    hide_sprite: !yellow_signal;
  }
  childsprite {
    sprite: signals(green);
    index: [0, 1];
    xoffset: [0, 16];
    hide_sprite: !green_signal;
  }
  building {
    sprite: platforms(concrete + platform_variation);
    index: [2, 4];
    yoffset: 12;
    yextent: 4;
  }
  // Same idea as first building's childsprites
}

spritelayout cobble_platform {
  // Same idea as concrete_platform layout
}    

spritelayout dirt_platform {
  // Same idea as concrete_platform layout
}

item(FEAT_STATIONS, concrete_station) {
  property { ... }
  graphics {
    concrete_platform;
  }
}

item(FEAT_STATIONS, cobble_station) {
  property { ... }
  graphics {
    cobble_platform;
  }
}

item(FEAT_STATIONS, dirt_station) {
  property { ... }
  graphics {
    dirt_platform;
  }
}

switch(FEAT_STATIONS, SELF, adaptation_switch, magic_expr_for_type) {
  1: cobble_platform;
  2: dirt_platform;
  concrete_platform;
}

item(FEAT_STATIONS, adaptive_station) {
  property { ... }
  graphics {
    adaptation_switch;
  }
}
  • `ground_track` is a macro that respects groundtype, snow/desert, and railtype
  • `sprite` may or may not support X/Y pair value, to be decided later
  • `(x|y)(offset|extent)` is swapped for Y orientation unless that property is already explicitly defined for the Y orientation
  • `index` is meant as a relative index added to the absolute index evaluated in `sprite`
  • `sprite` is one of:
    • <Expression>: evaluated TTD sprite index
    • <Spriteset>: first sprite of custom spriteset
    • <Spriteset>(<Spritelabel>): labelled index of custom spriteset
    • <Spriteset>(<Expression>): evaluated sprite index of custom spriteset (expression may use spritelabel)
    • <Spritegroup>: first sprite of spritegroup
    • <Spritegroup>(<Expression>): evaluated sprite index of spritegroup

#8 Updated by Elyon about 5 years ago

To avoid repetition in the NML source code from having nearly identical Action2Layout and StationSpritelayoutsProp classes, as well as to avoid having spritelayouts of stations handled by Action2Layout (which would be unfortunate, class-name wise), I propose changing the structure of the NML source slightly, by abstracting most of the functionality of the current Action2Layout class into an AbstractLayout class, and making use of this class both from Action2Layout and - in the case of stations - possibly from StationSpritelayoutsProp (and even StationAdvancedSpritelayoutsProp; see Questions#2 below) in "./actions/action0properties".

This abstraction would also allow using the same SpriteLayout for both stations and non-stations. While this will probably never be useful, I see no reason not to support this functionality if we have the means to do so without increasing NML source complexity by much; the supported spritelayout syntax for non-stations is a proper subset of the station spritelayout syntax, anyway.

Furthermore, while it would increase NML source complexity slightly, I personally find it resulting in a neater GRF/NFO to define property 09, 1A or 0A at the initial single station item Action0, as opposed to adding separate @Action0@s.

Finally, NML refuses to allow spritesets of different sizes being used in spritegroups and spritelayouts. While this makes sense for spritegroups (they're meant to reference variations of the same sprites, after all), I find that for spritelayouts, this limitation would be quite useful to overcome.

Specifically, the example NML file in note 7 (above) currently fails to compile since the station spritelayout references both spritesets and a spritegroup of different sizes. To fix this currently, the example NML file would either have to define every single sprite in the same spriteset, or add dummy sprites to every spriteset of smaller size than the largest set. Both approaches seem counter-intuitive to me and possibly GRF authors as well.

As such, I suggest overcoming the same-size limitation for spritelayouts (not spritegroups) - according to my understanding of GRF, this should definitely be possible with a bit of Action1 magic.

Questions:
  • Where should the AbstractLayout class reside? I would think "./actions/abstract_layout.py", but as it is the only abstract class - which doubles as part of a collection in a single property (for stations), and as a full Action2 in its own right - I am not quite sure where to place it; it is neither a possible property (only a part of one property), nor is it a guaranteed Action2 (ie. when it is used for a station).
  • Should NML support generating one of either property 09:Spritelayout or 1A:AdvancedSpritelayout (or 0A:CopySpritelayout when different station items have identical layout-lists), depending on whether a station's @spritelayout@s can be simplified to a property 09, or requires 1A to describe it?
  • Supposing NML is to support using the same spritelayout for stations and non-stations alike, how should property array values be treated for non-stations? The obvious implementation selects the first array value, and discards the second value entirely before compilation. Note: by happenstance, the `sprite` property does not currently fail to compile when given an array, but instead generates a switch that evaluates the first value, then discards that result and stores the result of evaluating the second value in a register; this register is then given as the sprite offset register for the now-forced-advanced spritelayout. I suggest either changing this to adhere to the obvious implementation of the other properties, or at the very least disallowing the definition of the sprite sprite property as an array for non-stations (and possibly disallowing an array for stations as well, to be decided later).

Also available in: Atom PDF