diff --git a/Classes.h b/Classes.h index f91c5e08..652b8564 100644 --- a/Classes.h +++ b/Classes.h @@ -45,6 +45,11 @@ struct light_array; class particle_manager; struct dictionary_source; +namespace plc { +using element_handle = short; +class basic_controller; +} + namespace scene { struct node_data; class basic_node; diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index b9b3fb93..04b00a28 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. //Q: 20160805 - odlaczenie pliku fizyki .pas od kompilacji #include #include "hamulce.h" +#include "ladderlogic.h" /* MaSzyna EU07 locomotive simulator Copyright (C) 2001-2004 Maciej Czapkiewicz and others @@ -1601,6 +1602,8 @@ public: int iProblem = 0; // flagi problemów z taborem, aby AI nie musiało porównywać; 0=może jechać int iLights[2]; // bity zapalonych świateł tutaj, żeby dało się liczyć pobór prądu + plc::basic_controller m_plc; + int AIHintPantstate{ 0 }; // suggested pantograph setup bool AIHintPantUpIfIdle{ true }; // whether raise both pantographs if idling for a while double AIHintLocalBrakeAccFactor{ 1.05 }; // suggested acceleration weight for local brake operation diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 065ce9df..55e260e0 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -1458,6 +1458,8 @@ void TMoverParameters::compute_movement_( double const Deltatime ) { // automatic doors update_doors( Deltatime ); + m_plc.update( Deltatime ); + PowerCouplersCheck( Deltatime, coupling::highvoltage ); PowerCouplersCheck( Deltatime, coupling::power110v ); PowerCouplersCheck( Deltatime, coupling::power24v ); diff --git a/eu07.ico b/eu07.ico index 8103741a..af04013c 100644 Binary files a/eu07.ico and b/eu07.ico differ diff --git a/ladderlogic.cpp b/ladderlogic.cpp new file mode 100644 index 00000000..8c31567d --- /dev/null +++ b/ladderlogic.cpp @@ -0,0 +1,394 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "ladderlogic.h" + +#include "parser.h" +#include "utilities.h" +#include "Logs.h" + +namespace plc { + +auto +basic_element::input() -> int & { + + switch( type ) { + case basic_element::type_e::variable: { + return data.variable.value; + } + case basic_element::type_e::timer: { + return data.timer.value; + } + case basic_element::type_e::counter: { + return data.counter.value; + } + } +} + +auto +basic_element::output() const -> int const { + + switch( type ) { + case basic_element::type_e::variable: { + return data.variable.value; + } + case basic_element::type_e::timer: { + return ( data.timer.time_elapsed >= data.timer.time_preset ? data.timer.value : 0 ); + } + case basic_element::type_e::counter: { + return ( data.counter.count_value >= data.counter.count_limit ? 1 : 0 ); + } + } +} + +auto +basic_controller::input( element_handle const Element ) -> int & { + + return m_elements[ Element - 1 ].input(); +} + +auto +basic_controller::output( element_handle const Element ) const -> int const { + + return m_elements[ Element - 1 ].output(); +} + +auto +basic_controller::load( std::string const &Filename ) -> bool { + + m_program.clear(); + m_updateaccumulator = 0.0; + + m_programfilename = Filename; + cParser input( m_programfilename, cParser::buffer_FILE ); + bool result { false }; + while( true == deserialize_operation( input ) ) { + result = true; // once would suffice but, eh + } + + return result; +} + +auto +basic_controller::update( double const Timestep ) -> int { + + if( false == m_timerhandles.empty() ) { + // update timers + m_updateaccumulator += Timestep; + auto const updatecount = std::floor( m_updateaccumulator / m_updaterate ); + if( updatecount > 0 ) { + auto const updateamount = static_cast( updatecount * 1000 * m_updaterate ); + for( auto const timerhandle : m_timerhandles ) { + auto &timer{ element( timerhandle ) }; + auto &timerdata{ timer.data.timer }; + if( timer.input() > 0 ) { + timerdata.time_elapsed = + std::min( + timerdata.time_preset, + timerdata.time_elapsed + updateamount ); + } + else { + timerdata.time_elapsed = 0; + } + } + m_updateaccumulator -= m_updaterate * updatecount; + } + } + + return run(); +} + +std::map const basic_controller::m_operationcodemap = { + { "ld", opcode_e::ld }, { "ldi", opcode_e::ldi }, + { "and", opcode_e::and }, { "ani", opcode_e::ani }, { "anb", opcode_e::anb }, + { "or", opcode_e::or }, { "ori", opcode_e::ori }, { "orb", opcode_e::orb }, + { "out", opcode_e::out }, { "set", opcode_e::set }, { "rst", opcode_e::rst }, + { "end", opcode_e::nop } +}; + +auto +basic_controller::deserialize_operation( cParser &Input ) -> bool { + + auto operationdata{ Input.getToken( true, "\n\r" ) }; + if( true == operationdata.empty() ) { return false; } + + operation operation = { opcode_e::nop, 0, 0, 0 }; + + cParser operationparser( operationdata, cParser::buffer_TEXT ); + // HACK: operation potentially contains 1-2 parameters so we try to grab the whole set + operationparser.getTokens( 3, "\t " ); + std::string + operationname, + operationelement, + operationparameter; + operationparser + >> operationname + >> operationelement + >> operationparameter; + + auto const lookup { m_operationcodemap.find( operationname ) }; + operation.code = ( + lookup != m_operationcodemap.end() ? + lookup->second : + opcode_e::nop ); + if( lookup == m_operationcodemap.end() ) { + log_error( "contains unknown command \"" + operationname + "\"", Input.Line() - 1 ); + } + + if( operation.code == opcode_e::nop ) { return true; } + + if( false == operationelement.empty() ) { + operation.element = + find_or_insert( + operationelement, + guess_element_type_from_name( operationelement ) ); + } + + if( false == operationparameter.empty() ) { + auto const parameter{ split_index( operationparameter ) }; + operation.parameter1 = static_cast( parameter.second ); + } + + m_program.emplace_back( operation ); + + return true; +} + +auto +basic_controller::insert( std::string const Name, basic_element Element ) -> element_handle { + + m_elements.push_back( Element ); + m_elementnames.push_back( Name ); + + auto const elementhandle{ static_cast( m_elements.size() ) }; + + // for timers make note of the element in the timer list + if( Element.type == basic_element::type_e::timer ) { + m_timerhandles.push_back( elementhandle ); + } + + return elementhandle; +} + +// runs one cycle of current program +auto +basic_controller::run() -> int { + + m_accumulator.clear(); + m_popstack = false; + auto programline { 1 }; + + for( auto const &operation : m_program ) { + // TBD: replace switch with function table for better readability/maintenance? + switch( operation.code ) { + + case opcode_e::ld: { + if( m_popstack ) { + if( false == m_accumulator.empty() ) { + m_accumulator.pop_back(); + } + m_popstack = false; + } + m_accumulator.emplace_back( output( operation.element ) ); + break; + } + + case opcode_e::ldi: { + if( m_popstack ) { + if( false == m_accumulator.empty() ) { + m_accumulator.pop_back(); + } + m_popstack = false; + } + m_accumulator.emplace_back( inverse( output( operation.element ) ) ); + break; + } + + case opcode_e::and: { + if( m_accumulator.empty() ) { + log_error( "attempted AND with empty accumulator", programline ); + break; + } + m_accumulator.back() &= output( operation.element ); + break; + } + + case opcode_e::ani: { + if( m_accumulator.empty() ) { + log_error( "attempted ANI with empty accumulator", programline ); + break; + } + m_accumulator.back() &= inverse( output( operation.element ) ); + break; + } + + case opcode_e::anb: { + if( m_accumulator.size() < 2 ) { + log_error( "attempted ANB with empty stack", programline ); + break; + } + auto const operand { m_accumulator.back() }; + m_accumulator.pop_back(); + m_accumulator.back() &= operand; + break; + } + + case opcode_e::or: { + if( m_accumulator.empty() ) { + log_error( "attempted OR with empty accumulator", programline ); + break; + } + m_accumulator.back() |= output( operation.element ); + break; + } + + case opcode_e::ori : { + if( m_accumulator.empty() ) { + log_error( "attempted ORI with empty accumulator", programline ); + break; + } + m_accumulator.back() |= inverse( output( operation.element ) ); + break; + } + + case opcode_e::orb: { + if( m_accumulator.size() < 2 ) { + log_error( "attempted ORB with empty stack", programline ); + break; + } + auto const operand{ m_accumulator.back() }; + m_accumulator.pop_back(); + m_accumulator.back() |= operand; + break; + } + + case opcode_e::out: { + if( m_accumulator.empty() ) { + log_error( "attempted OUT with empty accumulator", programline ); + break; + } + auto &target { element( operation.element ) }; + auto const initialstate { target.input() }; + target.input() = m_accumulator.back(); + // additional operations for advanced element types + switch( target.type ) { + case basic_element::type_e::timer: { + target.data.timer.time_preset = operation.parameter1; + break; + } + case basic_element::type_e::counter: { + target.data.counter.count_limit = operation.parameter1; + // increase counter value on input activation + if( ( initialstate == 0 ) && ( target.input() != 0 ) ) { +/* + // TBD: use overflow-prone version instead of safe one? + target.data.counter.count_value += 1; +*/ + target.data.counter.count_value = + std::min( + target.data.counter.count_limit, + target.data.counter.count_value + 1 ); + } + break; + } + } + // accumulator was published at least once, next ld(i) operation will start a new rung + m_popstack = true; + break; + } + + case opcode_e::set: { + if( m_accumulator.empty() ) { + log_error( "attempted SET with empty accumulator", programline ); + break; + } + if( m_accumulator.back() == 0 ) { + break; + } + auto &target { element( operation.element ) }; + auto const initialstate { target.input() }; + target.input() = m_accumulator.back(); + // additional operations for advanced element types + switch( target.type ) { + case basic_element::type_e::counter: { + // NOTE: siemens counter behavior + // TODO: check whether this is true for mitsubishi + target.data.counter.count_limit = target.data.counter.count_value; +/* + if( ( initialstate == 0 ) && ( target.input() != 0 ) ) { + target.data.counter.count_value = + std::min( + target.data.counter.count_limit, + target.data.counter.count_value + 1 ); + } +*/ + break; + } + } + // accumulator was published at least once, next ld(i) operation will start a new rung + m_popstack = true; + break; + } + + case opcode_e::rst: { + if( m_accumulator.empty() ) { + log_error( "attempted RST with empty accumulator", programline ); + break; + } + if( m_accumulator.back() == 0 ) { + break; + } + auto &target{ element( operation.element ) }; + target.input() = 0; + // additional operations for advanced element types + switch( target.type ) { + case basic_element::type_e::counter: { + target.data.counter.count_value = 0; + break; + } + } + // accumulator was published at least once, next ld(i) operation will start a new rung + m_popstack = true; + break; + } + } + ++programline; + } + + return 0; +} + +void +basic_controller::log_error( std::string const &Error, int const Line ) const { + + ErrorLog( + "Bad plc program: \"" + m_programfilename + "\" " + + Error + + ( Line > 0 ? + " (line " + to_string( Line ) + ")" : + "" ) ); +} + +auto +basic_controller::guess_element_type_from_name( std::string const &Name ) const -> basic_element::type_e { + + auto const name { split_index( Name ) }; + + if( ( name.first == "t" ) || ( name.first == "ton" ) || ( name.first.find( "timer." ) == 0 ) ) { + return basic_element::type_e::timer; + } + if( ( name.first == "c" ) || ( name.first.find( "counter." ) == 0 ) ) { + return basic_element::type_e::counter; + } + + return basic_element::type_e::variable; +} + +} // plc diff --git a/ladderlogic.h b/ladderlogic.h new file mode 100644 index 00000000..40e3d7a2 --- /dev/null +++ b/ladderlogic.h @@ -0,0 +1,173 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "Classes.h" + +namespace plc { + +using element_handle = short; + +// basic logic element. +class basic_element { +public: +// types + // rtti + enum class type_e { + variable, + timer, + counter, + }; +// constructors + template + basic_element( basic_element::type_e = basic_element::type_e::variable Type, Args_ ...Args ); +// methods + // data access + auto input() -> int &; + auto output() const -> int const; +private: +// types + // cell content variants + struct variable { + std::int32_t value; + }; + struct timer { + std::int32_t value; + short time_preset; + short time_elapsed; + }; + struct counter { + std::int32_t value; + short count_limit; + short count_value; + }; +// members + type_e type; + union { + variable variable; + timer timer; + counter counter; + } data; +// friends + friend class basic_controller; +}; + +class basic_controller { + +public: +// methods + auto load( std::string const &Filename ) -> bool; + auto update( double const Timestep ) -> int; + // finds element with specified name, potentially creating new element of specified type initialized with provided arguments. returns: handle to the element + template + auto find_or_insert( std::string const &Name, basic_element::type_e Type = basic_element::type_e::variable, Args_ ...Args ) -> element_handle; + // data access + auto input( element_handle const Element ) -> int &; + auto output( element_handle const Element ) const -> int const; + +private: +//types + // plc program instruction + enum class opcode_e : short { + nop, + ld, + ldi, + and, + ani, + anb, + or, + ori, + orb, + out, + set, + rst, + }; + struct operation { + opcode_e code; + short element; + short parameter1; + short parameter2; + }; + // containers + using element_sequence = std::vector; + using name_sequence = std::vector; + using operation_sequence = std::vector; + using handle_sequence = std::vector; +// methods + auto deserialize_operation( cParser &Input ) -> bool; + // adds provided item to the collection. returns: true if there's no duplicate with the same name, false otherwise + auto insert( std::string const Name, basic_element Element ) -> element_handle; + // runs one cycle of current program. returns: error code or 0 if there's no error + auto run() -> int; + void log_error( std::string const &Error, int const Line = -1 ) const; + auto guess_element_type_from_name( std::string const &Name ) const->basic_element::type_e; + inline + auto inverse( int const Value ) const -> int { + return ( Value == 0 ? 1 : 0 ); } + // element access + inline + auto element( element_handle const Element ) const -> basic_element const { + return m_elements[ Element - 1 ]; } + inline + auto element( element_handle const Element ) -> basic_element & { + return m_elements[ Element - 1 ]; } +// members + static std::map const m_operationcodemap; + element_sequence m_elements; // collection of elements accessed by the plc program + name_sequence m_elementnames; + handle_sequence m_timerhandles; // indices of timer elements, timer update optimization helper + std::string m_programfilename; // cached filename of currently loaded program + operation_sequence m_program; // current program for the plc + std::vector m_accumulator; // state accumulator for currently processed program rung + bool m_popstack { false }; // whether ld(i) operation should pop the accumulator stack or just add onto it + double m_updateaccumulator { 0.0 }; // + double m_updaterate { 0.1 }; +}; + +template +basic_element::basic_element( basic_element::type_e Type, Args_ ...Args ) +: type{ Type } +{ + switch( type ) { + case type_e::variable: { + data.variable = variable{ Args ... }; + break; + } + case type_e::timer: { + data.timer = timer{ Args ... }; + break; + } + case type_e::counter: { + data.counter = counter{ Args ... }; + break; + } + default: { + // TBD: log error if we get here? + break; + } + } +} + +template +auto basic_controller::find_or_insert( std::string const &Name, basic_element::type_e Type, Args_ ...Args ) -> element_handle { + // NOTE: because we expect all lookups to be performed only (once) during controller (code) initialization + // we're using simple linear container for names, to allow for easy access to both elements and their names with the same handle + auto index { 1 }; + for( auto const &name : m_elementnames ) { + if( name == Name ) { + return index; + } + ++index; + } + // create and insert a new element if we didn't find existing one + return insert( Name, basic_element( Type, Args ... ) ); +} + +} // plc diff --git a/maszyna.vcxproj b/maszyna.vcxproj index 7377c065..58782cd2 100644 --- a/maszyna.vcxproj +++ b/maszyna.vcxproj @@ -217,6 +217,7 @@ + @@ -396,6 +397,7 @@ + diff --git a/maszyna.vcxproj.filters b/maszyna.vcxproj.filters index 0a713f73..05f37d89 100644 --- a/maszyna.vcxproj.filters +++ b/maszyna.vcxproj.filters @@ -465,6 +465,9 @@ Source Files\application\network\backend + + Source Files + @@ -851,6 +854,9 @@ Header Files + + Header Files + diff --git a/material.cpp b/material.cpp index a2cd1fd3..50052bb5 100644 --- a/material.cpp +++ b/material.cpp @@ -483,7 +483,7 @@ material_manager::create( std::string const &Filename, bool const Loadnow ) { erase_leading_slashes( filename ); } - auto const databanklookup { find_in_databank( ToLower( filename ) ) }; + auto const databanklookup { find_in_databank( filename ) }; if( databanklookup != null_handle ) { return databanklookup; } @@ -528,7 +528,7 @@ material_manager::create( std::string const &Filename, bool const Loadnow ) { */ // HACK: create parse info for material finalize() method cParser materialparser( - "texture1: " + Filename, + "texture1: \"" + Filename + "\"", cParser::buffer_TEXT ); material.deserialize( materialparser, Loadnow ); } diff --git a/parser.cpp b/parser.cpp index 1ba87a48..bc7ec1f5 100644 --- a/parser.cpp +++ b/parser.cpp @@ -288,7 +288,7 @@ void cParser::skipComment( std::string const &Endmark ) { // pobieranie znaków ++mLine; } input += c; - if( input.find( Endmark ) != std::string::npos ) // szukanie znacznika końca + if( input == Endmark ) // szukanie znacznika końca break; if( input.size() >= endmarksize ) { // keep the read text short, to avoid pointless string re-allocations on longer comments @@ -300,9 +300,9 @@ void cParser::skipComment( std::string const &Endmark ) { // pobieranie znaków bool cParser::findQuotes( std::string &String ) { - if( String.rfind( '\"' ) != std::string::npos ) { + if( String.back() == '\"' ) { - String.erase( String.rfind( '\"' ), 1 ); + String.pop_back(); String += readQuotes(); return true; } @@ -311,12 +311,14 @@ bool cParser::findQuotes( std::string &String ) { bool cParser::trimComments(std::string &String) { - for (commentmap::iterator cmIt = mComments.begin(); cmIt != mComments.end(); ++cmIt) + for (auto const &comment : mComments) { - if (String.rfind((*cmIt).first) != std::string::npos) + if( String.size() < comment.first.size() ) { continue; } + + if (String.compare( String.size() - comment.first.size(), comment.first.size(), comment.first ) == 0) { - skipComment((*cmIt).second); - String.resize(String.rfind((*cmIt).first)); + skipComment(comment.second); + String.resize(String.rfind(comment.first)); return true; } }