/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
*/
/** @file tracerestrict.cpp Main file for Trace Restrict */
#include "stdafx.h"
#include "tracerestrict.h"
#include "train.h"
#include "core/bitmath_func.hpp"
#include "core/pool_func.hpp"
#include "command_func.h"
#include "company_func.h"
#include "viewport_func.h"
#include "window_func.h"
#include "order_base.h"
#include "cargotype.h"
#include "group.h"
#include "pathfinder/yapf/yapf_cache.h"
#include
#include
/** @file
*
* Trace Restrict Data Storage Model Notes:
*
* Signals may have 0, 1 or 2 trace restrict programs attached to them,
* up to one for each track. Two-way signals share the same program.
*
* The mapping between signals and programs is defined in terms of
* TraceRestrictRefId to TraceRestrictProgramID,
* where TraceRestrictRefId is formed of the tile index and track,
* and TraceRestrictProgramID is an index into the program pool.
*
* If one or more mappings exist for a given signal tile, bit 12 of M3 will be set to 1.
* This is updated whenever mappings are added/removed for that tile. This is to avoid
* needing to do a mapping lookup for the common case where there is no trace restrict
* program mapping for the given tile.
*
* Programs in the program pool are refcounted based on the number of mappings which exist.
* When this falls to 0, the program is deleted from the pool.
* If a program has a refcount greater than 1, it is a shared program.
*
* In all cases, an empty program is evaluated the same as the absence of a program.
* Therefore it is not necessary to store mappings to empty unshared programs.
* Any editing action which would otherwise result in a mapping to an empty program
* which has no other references, instead removes the mapping.
* This is not done for shared programs as this would delete the shared aspect whenever
* the program became empty.
*
* Special case: In the case where an empty program with refcount 2 has one of its
* mappings removed, the other mapping is left pointing to an empty unshared program.
* This other mapping is then removed by performing a linear search of the mappings,
* and removing the reference to that program ID.
*/
TraceRestrictProgramPool _tracerestrictprogram_pool("TraceRestrictProgram");
INSTANTIATE_POOL_METHODS(TraceRestrictProgram)
/**
* TraceRestrictRefId --> TraceRestrictProgramID (Pool ID) mapping
* The indirection is mainly to enable shared programs
* TODO: use a more efficient container/indirection mechanism
*/
TraceRestrictMapping _tracerestrictprogram_mapping;
/**
* List of pre-defined pathfinder penalty values
* This is indexed by TraceRestrictPathfinderPenaltyPresetIndex
*/
const uint16 _tracerestrict_pathfinder_penalty_preset_values[] = {
500,
2000,
8000,
};
assert_compile(lengthof(_tracerestrict_pathfinder_penalty_preset_values) == TRPPPI_END);
/**
* This should be used when all pools have been or are immediately about to be also cleared
* Calling this at other times will leave dangling refcounts
*/
void ClearTraceRestrictMapping() {
_tracerestrictprogram_mapping.clear();
}
/**
* Flags used for the program execution condition stack
* Each 'if' pushes onto the stack
* Each 'end if' pops from the stack
* Elif/orif/else may modify the stack top
*/
enum TraceRestrictCondStackFlags {
TRCSF_DONE_IF = 1<<0, ///< The if/elif/else is "done", future elif/else branches will not be executed
TRCSF_SEEN_ELSE = 1<<1, ///< An else branch has been seen already, error if another is seen afterwards
TRCSF_ACTIVE = 1<<2, ///< The condition is currently active
TRCSF_PARENT_INACTIVE = 1<<3, ///< The parent condition is not active, thus this condition is also not active
};
DECLARE_ENUM_AS_BIT_SET(TraceRestrictCondStackFlags)
/**
* Helper function to handle condition stack manipulatoin
*/
static void HandleCondition(std::vector &condstack, TraceRestrictCondFlags condflags, bool value)
{
if (condflags & TRCF_OR) {
assert(!condstack.empty());
if (condstack.back() & TRCSF_ACTIVE) {
// leave TRCSF_ACTIVE set
return;
}
}
if (condflags & (TRCF_OR | TRCF_ELSE)) {
assert(!condstack.empty());
if (condstack.back() & (TRCSF_DONE_IF | TRCSF_PARENT_INACTIVE)) {
condstack.back() &= ~TRCSF_ACTIVE;
return;
}
} else {
if (!condstack.empty() && !(condstack.back() & TRCSF_ACTIVE)) {
//this is a 'nested if', the 'parent if' is not active
condstack.push_back(TRCSF_PARENT_INACTIVE);
return;
}
condstack.push_back(static_cast(0));
}
if (value) {
condstack.back() |= TRCSF_DONE_IF | TRCSF_ACTIVE;
} else {
condstack.back() &= ~TRCSF_ACTIVE;
}
}
/**
* Integer condition testing
* Test value op condvalue
*/
static bool TestCondition(uint16 value, TraceRestrictCondOp condop, uint16 condvalue)
{
switch (condop) {
case TRCO_IS:
return value == condvalue;
case TRCO_ISNOT:
return value != condvalue;
case TRCO_LT:
return value < condvalue;
case TRCO_LTE:
return value <= condvalue;
case TRCO_GT:
return value > condvalue;
case TRCO_GTE:
return value >= condvalue;
default:
NOT_REACHED();
return false;
}
}
/**
* Binary condition testing helper function
*/
static bool TestBinaryConditionCommon(TraceRestrictItem item, bool input)
{
switch (GetTraceRestrictCondOp(item)) {
case TRCO_IS:
return input;
case TRCO_ISNOT:
return !input;
default:
NOT_REACHED();
return false;
}
}
/**
* Test order condition
* @p order may be NULL
*/
static bool TestOrderCondition(const Order *order, TraceRestrictItem item)
{
bool result = false;
if (order) {
DestinationID condvalue = GetTraceRestrictValue(item);
switch (static_cast(GetTraceRestrictAuxField(item))) {
case TROCAF_STATION:
result = order->IsType(OT_GOTO_STATION) && order->GetDestination() == condvalue;
break;
case TROCAF_WAYPOINT:
result = order->IsType(OT_GOTO_WAYPOINT) && order->GetDestination() == condvalue;
break;
case TROCAF_DEPOT:
result = order->IsType(OT_GOTO_DEPOT) && order->GetDestination() == condvalue;
break;
default:
NOT_REACHED();
}
}
return TestBinaryConditionCommon(item, result);
}
/**
* Test station condition
*/
static bool TestStationCondition(StationID station, TraceRestrictItem item)
{
bool result = (GetTraceRestrictAuxField(item) == TROCAF_STATION) && (station == GetTraceRestrictValue(item));
return TestBinaryConditionCommon(item, result);
}
/**
* Execute program on train and store results in out
* @p v may not be NULL
* @p out should be zero-initialised
*/
void TraceRestrictProgram::Execute(const Train* v, const TraceRestrictProgramInput &input, TraceRestrictProgramResult& out) const
{
// static to avoid needing to re-alloc/resize on each execution
static std::vector condstack;
condstack.clear();
bool have_previous_signal = false;
TileIndex previous_signal_tile = INVALID_TILE;
size_t size = this->items.size();
for (size_t i = 0; i < size; i++) {
TraceRestrictItem item = this->items[i];
TraceRestrictItemType type = GetTraceRestrictType(item);
if (IsTraceRestrictConditional(item)) {
TraceRestrictCondFlags condflags = GetTraceRestrictCondFlags(item);
TraceRestrictCondOp condop = GetTraceRestrictCondOp(item);
if (type == TRIT_COND_ENDIF) {
assert(!condstack.empty());
if (condflags & TRCF_ELSE) {
// else
assert(!(condstack.back() & TRCSF_SEEN_ELSE));
HandleCondition(condstack, condflags, true);
condstack.back() |= TRCSF_SEEN_ELSE;
} else {
// end if
condstack.pop_back();
}
} else {
uint16 condvalue = GetTraceRestrictValue(item);
bool result = false;
switch(type) {
case TRIT_COND_UNDEFINED:
result = false;
break;
case TRIT_COND_TRAIN_LENGTH:
result = TestCondition(CeilDiv(v->gcache.cached_total_length, TILE_SIZE), condop, condvalue);
break;
case TRIT_COND_MAX_SPEED:
result = TestCondition(v->GetDisplayMaxSpeed(), condop, condvalue);
break;
case TRIT_COND_CURRENT_ORDER:
result = TestOrderCondition(&(v->current_order), item);
break;
case TRIT_COND_NEXT_ORDER: {
if (v->orders.list == NULL) break;
if (v->orders.list->GetNumOrders() == 0) break;
const Order *current_order = v->GetOrder(v->cur_real_order_index);
for (const Order *order = v->orders.list->GetNext(current_order); order != current_order; order = v->orders.list->GetNext(order)) {
if (order->IsGotoOrder()) {
result = TestOrderCondition(order, item);
break;
}
}
break;
}
case TRIT_COND_LAST_STATION:
result = TestStationCondition(v->last_station_visited, item);
break;
case TRIT_COND_CARGO: {
bool have_cargo = false;
for (const Vehicle *v_iter = v; v_iter != NULL; v_iter = v_iter->Next()) {
if (v_iter->cargo_type == GetTraceRestrictValue(item) && v_iter->cargo_cap > 0) {
have_cargo = true;
break;
}
}
result = TestBinaryConditionCommon(item, have_cargo);
break;
}
case TRIT_COND_ENTRY_DIRECTION: {
bool direction_match;
switch (GetTraceRestrictValue(item)) {
case TRNTSV_NE:
case TRNTSV_SE:
case TRNTSV_SW:
case TRNTSV_NW:
direction_match = (static_cast(GetTraceRestrictValue(item)) == TrackdirToExitdir(ReverseTrackdir(input.trackdir)));
break;
case TRDTSV_FRONT:
direction_match = IsTileType(input.tile, MP_RAILWAY) && HasSignalOnTrackdir(input.tile, input.trackdir);
break;
case TRDTSV_BACK:
direction_match = IsTileType(input.tile, MP_RAILWAY) && !HasSignalOnTrackdir(input.tile, input.trackdir);
break;
default:
NOT_REACHED();
break;
}
result = TestBinaryConditionCommon(item, direction_match);
break;
}
case TRIT_COND_PBS_ENTRY_SIGNAL: {
// TRVT_TILE_INDEX value type uses the next slot
i++;
uint32_t signal_tile = this->items[i];
if (!have_previous_signal) {
if (input.previous_signal_callback) {
previous_signal_tile = input.previous_signal_callback(v, input.previous_signal_ptr);
}
have_previous_signal = true;
}
bool match = (signal_tile != INVALID_TILE)
&& (previous_signal_tile == signal_tile);
result = TestBinaryConditionCommon(item, match);
break;
}
case TRIT_COND_TRAIN_GROUP: {
result = TestBinaryConditionCommon(item, GroupIsInGroup(v->group_id, GetTraceRestrictValue(item)));
break;
}
case TRIT_COND_PHYS_PROP: {
switch (static_cast(GetTraceRestrictAuxField(item))) {
case TRPPCAF_WEIGHT:
result = TestCondition(v->gcache.cached_weight, condop, condvalue);
break;
case TRPPCAF_POWER:
result = TestCondition(v->gcache.cached_power, condop, condvalue);
break;
case TRPPCAF_MAX_TE:
result = TestCondition(v->gcache.cached_max_te / 1000, condop, condvalue);
break;
default:
NOT_REACHED();
break;
}
break;
}
case TRIT_COND_PHYS_RATIO: {
switch (static_cast(GetTraceRestrictAuxField(item))) {
case TRPPRCAF_POWER_WEIGHT:
result = TestCondition(min(UINT16_MAX, (100 * v->gcache.cached_power) / max(1, v->gcache.cached_weight)), condop, condvalue);
break;
case TRPPRCAF_MAX_TE_WEIGHT:
result = TestCondition(min(UINT16_MAX, (v->gcache.cached_max_te / 10) / max(1, v->gcache.cached_weight)), condop, condvalue);
break;
default:
NOT_REACHED();
break;
}
break;
}
default:
NOT_REACHED();
}
HandleCondition(condstack, condflags, result);
}
} else {
if (condstack.empty() || condstack.back() & TRCSF_ACTIVE) {
switch(type) {
case TRIT_PF_DENY:
if (GetTraceRestrictValue(item)) {
out.flags &= ~TRPRF_DENY;
} else {
out.flags |= TRPRF_DENY;
}
break;
case TRIT_PF_PENALTY:
switch (static_cast(GetTraceRestrictAuxField(item))) {
case TRPPAF_VALUE:
out.penalty += GetTraceRestrictValue(item);
break;
case TRPPAF_PRESET: {
uint16 index = GetTraceRestrictValue(item);
assert(index < TRPPPI_END);
out.penalty += _tracerestrict_pathfinder_penalty_preset_values[index];
break;
}
default:
NOT_REACHED();
}
break;
case TRIT_RESERVE_THROUGH:
if (GetTraceRestrictValue(item)) {
out.flags &= ~TRPRF_RESERVE_THROUGH;
} else {
out.flags |= TRPRF_RESERVE_THROUGH;
}
break;
case TRIT_LONG_RESERVE:
if (GetTraceRestrictValue(item)) {
out.flags &= ~TRPRF_LONG_RESERVE;
} else {
out.flags |= TRPRF_LONG_RESERVE;
}
break;
case TRIT_WAIT_AT_PBS:
if (GetTraceRestrictValue(item)) {
out.flags &= ~TRPRF_WAIT_AT_PBS;
} else {
out.flags |= TRPRF_WAIT_AT_PBS;
}
break;
default:
NOT_REACHED();
}
}
}
}
assert(condstack.empty());
}
/**
* Decrement ref count, only use when removing a mapping
*/
void TraceRestrictProgram::DecrementRefCount() {
assert(this->refcount > 0);
this->refcount--;
if (this->refcount == 0) {
delete this;
}
}
/**
* Validate a instruction list
* Returns successful result if program seems OK
* This only validates that conditional nesting is correct,
* and that all instructions have a known type, at present
*/
CommandCost TraceRestrictProgram::Validate(const std::vector &items, TraceRestrictProgramActionsUsedFlags &actions_used_flags) {
// static to avoid needing to re-alloc/resize on each execution
static std::vector condstack;
condstack.clear();
actions_used_flags = static_cast(0);
size_t size = items.size();
for (size_t i = 0; i < size; i++) {
TraceRestrictItem item = items[i];
TraceRestrictItemType type = GetTraceRestrictType(item);
// check multi-word instructions
if (IsTraceRestrictDoubleItem(item)) {
i++;
if (i >= size) {
return_cmd_error(STR_TRACE_RESTRICT_ERROR_OFFSET_TOO_LARGE); // instruction ran off end
}
}
if (IsTraceRestrictConditional(item)) {
TraceRestrictCondFlags condflags = GetTraceRestrictCondFlags(item);
if (type == TRIT_COND_ENDIF) {
if (condstack.empty()) {
return_cmd_error(STR_TRACE_RESTRICT_ERROR_VALIDATE_NO_IF); // else/endif with no starting if
}
if (condflags & TRCF_ELSE) {
// else
if (condstack.back() & TRCSF_SEEN_ELSE) {
return_cmd_error(STR_TRACE_RESTRICT_ERROR_VALIDATE_DUP_ELSE); // Two else clauses
}
HandleCondition(condstack, condflags, true);
condstack.back() |= TRCSF_SEEN_ELSE;
} else {
// end if
condstack.pop_back();
}
} else {
if (condflags & (TRCF_OR | TRCF_ELSE)) { // elif/orif
if (condstack.empty()) {
return_cmd_error(STR_TRACE_RESTRICT_ERROR_VALIDATE_ELIF_NO_IF); // Pre-empt assertions in HandleCondition
}
if (condstack.back() & TRCSF_SEEN_ELSE) {
return_cmd_error(STR_TRACE_RESTRICT_ERROR_VALIDATE_DUP_ELSE); // else clause followed by elif/orif
}
}
HandleCondition(condstack, condflags, true);
}
switch (GetTraceRestrictType(item)) {
case TRIT_COND_ENDIF:
case TRIT_COND_UNDEFINED:
case TRIT_COND_TRAIN_LENGTH:
case TRIT_COND_MAX_SPEED:
case TRIT_COND_CURRENT_ORDER:
case TRIT_COND_NEXT_ORDER:
case TRIT_COND_LAST_STATION:
case TRIT_COND_CARGO:
case TRIT_COND_ENTRY_DIRECTION:
case TRIT_COND_PBS_ENTRY_SIGNAL:
case TRIT_COND_TRAIN_GROUP:
case TRIT_COND_PHYS_PROP:
case TRIT_COND_PHYS_RATIO:
break;
default:
return_cmd_error(STR_TRACE_RESTRICT_ERROR_VALIDATE_UNKNOWN_INSTRUCTION);
}
} else {
switch (GetTraceRestrictType(item)) {
case TRIT_PF_DENY:
case TRIT_PF_PENALTY:
actions_used_flags |= TRPAUF_PF;
break;
case TRIT_RESERVE_THROUGH:
actions_used_flags |= TRPAUF_RESERVE_THROUGH;
break;
case TRIT_LONG_RESERVE:
actions_used_flags |= TRPAUF_LONG_RESERVE;
break;
case TRIT_WAIT_AT_PBS:
actions_used_flags |= TRPAUF_WAIT_AT_PBS;
break;
default:
return_cmd_error(STR_TRACE_RESTRICT_ERROR_VALIDATE_UNKNOWN_INSTRUCTION);
}
}
}
if(!condstack.empty()) {
return_cmd_error(STR_TRACE_RESTRICT_ERROR_VALIDATE_END_CONDSTACK);
}
return CommandCost();
}
/**
* Convert an instruction index into an item array index
*/
size_t TraceRestrictProgram::InstructionOffsetToArrayOffset(const std::vector &items, size_t offset)
{
size_t output_offset = 0;
size_t size = items.size();
for (size_t i = 0; i < offset && output_offset < size; i++, output_offset++) {
if (IsTraceRestrictDoubleItem(items[output_offset])) {
output_offset++;
}
}
return output_offset;
}
/**
* Convert an item array index into an instruction index
*/
size_t TraceRestrictProgram::ArrayOffsetToInstructionOffset(const std::vector &items, size_t offset)
{
size_t output_offset = 0;
for (size_t i = 0; i < offset; i++, output_offset++) {
if (IsTraceRestrictDoubleItem(items[i])) {
i++;
}
}
return output_offset;
}
/**
* Set the value and aux field of @p item, as per the value type in @p value_type
*/
void SetTraceRestrictValueDefault(TraceRestrictItem &item, TraceRestrictValueType value_type)
{
switch (value_type) {
case TRVT_NONE:
case TRVT_INT:
case TRVT_DENY:
case TRVT_SPEED:
case TRVT_TILE_INDEX:
case TRVT_RESERVE_THROUGH:
case TRVT_LONG_RESERVE:
case TRVT_WEIGHT:
case TRVT_POWER:
case TRVT_FORCE:
case TRVT_POWER_WEIGHT_RATIO:
case TRVT_FORCE_WEIGHT_RATIO:
case TRVT_WAIT_AT_PBS:
SetTraceRestrictValue(item, 0);
if (!IsTraceRestrictTypeAuxSubtype(GetTraceRestrictType(item))) {
SetTraceRestrictAuxField(item, 0);
}
break;
case TRVT_ORDER:
SetTraceRestrictValue(item, INVALID_STATION);
SetTraceRestrictAuxField(item, TROCAF_STATION);
break;
case TRVT_CARGO_ID:
assert(_standard_cargo_mask != 0);
SetTraceRestrictValue(item, FindFirstBit64(_standard_cargo_mask));
SetTraceRestrictAuxField(item, 0);
break;
case TRVT_DIRECTION:
SetTraceRestrictValue(item, TRDTSV_FRONT);
SetTraceRestrictAuxField(item, 0);
break;
case TRVT_PF_PENALTY:
SetTraceRestrictValue(item, TRPPPI_SMALL);
SetTraceRestrictAuxField(item, TRPPAF_PRESET);
break;
case TRVT_GROUP_INDEX:
SetTraceRestrictValue(item, INVALID_GROUP);
SetTraceRestrictAuxField(item, 0);
break;
default:
NOT_REACHED();
break;
}
}
/**
* Set the type field of a TraceRestrictItem, and resets any other fields which are no longer valid/meaningful to sensible defaults
*/
void SetTraceRestrictTypeAndNormalise(TraceRestrictItem &item, TraceRestrictItemType type, uint8 aux_data)
{
if (item != 0) {
assert(GetTraceRestrictType(item) != TRIT_NULL);
assert(IsTraceRestrictConditional(item) == IsTraceRestrictTypeConditional(type));
}
assert(type != TRIT_NULL);
TraceRestrictTypePropertySet old_properties = GetTraceRestrictTypeProperties(item);
SetTraceRestrictType(item, type);
if (IsTraceRestrictTypeAuxSubtype(type)) {
SetTraceRestrictAuxField(item, aux_data);
} else {
assert(aux_data == 0);
}
TraceRestrictTypePropertySet new_properties = GetTraceRestrictTypeProperties(item);
if (old_properties.cond_type != new_properties.cond_type ||
old_properties.value_type != new_properties.value_type) {
SetTraceRestrictCondOp(item, TRCO_IS);
SetTraceRestrictValueDefault(item, new_properties.value_type);
}
if (GetTraceRestrictType(item) == TRIT_COND_LAST_STATION && GetTraceRestrictAuxField(item) != TROCAF_STATION) {
// if changing type from another order type to last visited station, reset value if not currently a station
SetTraceRestrictValueDefault(item, TRVT_ORDER);
}
}
/**
* Sets the "signal has a trace restrict mapping" bit
* This looks for mappings with that tile index
*/
void SetIsSignalRestrictedBit(TileIndex t)
{
// First mapping for this tile, or later
TraceRestrictMapping::iterator lower_bound = _tracerestrictprogram_mapping.lower_bound(MakeTraceRestrictRefId(t, static_cast