diff --git a/source/code/core/collections/public/ice/container/array.hxx b/source/code/core/collections/public/ice/container/array.hxx index 7153b33e..3f80208b 100644 --- a/source/code/core/collections/public/ice/container/array.hxx +++ b/source/code/core/collections/public/ice/container/array.hxx @@ -49,6 +49,10 @@ namespace ice::array requires std::copy_constructible inline void push_back(ice::Array& arr, ice::Span items) noexcept; + template + requires std::copy_constructible && (std::is_same_v == false) + inline void push_back(ice::Array& arr, ice::Span items, Type(*fn)(Source const&) noexcept) noexcept; + template inline void pop_back(ice::Array& arr, ice::ucount count = 1) noexcept; @@ -123,6 +127,9 @@ namespace ice::array template inline auto memset(ice::Array& arr, ice::u8 value) noexcept -> ice::Memory; + template + inline auto meminfo(ice::Array const& arr) noexcept -> ice::meminfo; + } // namespace ice::array namespace ice diff --git a/source/code/core/collections/public/ice/container/impl/array_impl.inl b/source/code/core/collections/public/ice/container/impl/array_impl.inl index 462b827f..356915c5 100644 --- a/source/code/core/collections/public/ice/container/impl/array_impl.inl +++ b/source/code/core/collections/public/ice/container/impl/array_impl.inl @@ -376,6 +376,23 @@ namespace ice arr._count += ice::span::count(items); } + template + requires std::copy_constructible && (std::is_same_v == false) + inline void push_back(ice::Array& arr, ice::Span items, Type(*fn)(Source const&) noexcept) noexcept + { + ice::ucount const required_capacity = arr._count + ice::span::count(items); + if (required_capacity > arr._capacity) + { + ice::array::grow(arr, required_capacity); + } + + ice::ucount const missing_items = required_capacity - arr._count; + for (ice::u32 src_idx = 0; src_idx < missing_items; ++src_idx) + { + ice::array::push_back(arr, fn(items[src_idx])); + } + } + template inline void pop_back(ice::Array& arr, ice::ucount count /*= 1*/) noexcept { @@ -466,9 +483,15 @@ namespace ice } template - inline auto slice(ice::Array const& arr, ice::ucount from_idx, ice::ucount to) noexcept -> ice::Span + inline auto slice(ice::Array const& arr, ice::ucount from_idx, ice::ucount count) noexcept -> ice::Span + { + return ice::span::subspan(arr, from_idx, count); + } + + template + inline auto slice(ice::Array const& arr, ice::ref32 ref) noexcept -> ice::Span { - return ice::span::subspan(arr, from_idx, to); + return ice::span::subspan(arr, ref); } template @@ -539,6 +562,12 @@ namespace ice return mem; } + template + inline auto meminfo(ice::Array const& arr) noexcept -> ice::meminfo + { + return ice::meminfo_of * ice::array::count(arr); + } + } // namespace array } // namespace ice diff --git a/source/code/core/collections/public/ice/span.hxx b/source/code/core/collections/public/ice/span.hxx index fad9c7f8..938454ce 100644 --- a/source/code/core/collections/public/ice/span.hxx +++ b/source/code/core/collections/public/ice/span.hxx @@ -85,6 +85,9 @@ namespace ice template constexpr auto subspan(ice::Span span, ice::ucount from_idx, ice::ucount count = ice::ucount_max) noexcept -> ice::Span; + template + constexpr auto subspan(ice::Span span, ice::ref32 ref) noexcept -> ice::Span; + template constexpr auto begin(ice::Span span) noexcept -> typename ice::Span::Iterator; @@ -257,6 +260,12 @@ namespace ice return { span._data + from_start, new_count }; } + template + constexpr auto subspan(ice::Span span, ice::ref32 ref) noexcept -> ice::Span + { + return ice::span::subspan(span, ref.offset, ref.size); + } + template constexpr auto begin(ice::Span span) noexcept -> typename ice::Span::Iterator { @@ -330,12 +339,26 @@ namespace ice } template - constexpr auto from_memory(ice::Memory const& mem, ice::meminfo meminfo, ice::usize offset) noexcept + constexpr auto from_memory(ice::Memory const& mem, ice::ucount count, ice::usize offset) noexcept -> ice::Span { + static ice::meminfo minfo = ice::meminfo_of; + void* const ptr = ice::ptr_add(mem.location, offset); - ICE_ASSERT_CORE(ice::is_aligned(ptr, meminfo.alignment)); - ICE_ASSERT_CORE(ice::ptr_add(mem.location, mem.size) >= ice::ptr_add(ptr, meminfo.size)); - return ice::Span{ reinterpret_cast(ptr), ice::ucount((meminfo.size / ice::size_of).value) }; + ICE_ASSERT_CORE(ice::is_aligned(ptr, minfo.alignment)); + ICE_ASSERT_CORE(ice::ptr_add(mem.location, mem.size) >= ice::ptr_add(ptr, minfo.size * count)); + return ice::Span{ reinterpret_cast(ptr), count }; + } + + // TODO: Move to another location or rename? Not sure this is properly named + template + constexpr auto from_data(ice::Data const& mem, ice::ucount count, ice::usize offset) noexcept -> ice::Span + { + static ice::meminfo constexpr minfo = ice::meminfo_of; + + void const* const ptr = ice::ptr_add(mem.location, offset); + ICE_ASSERT_CORE(ice::is_aligned(ptr, minfo.alignment)); + ICE_ASSERT_CORE(ice::ptr_add(mem.location, mem.size) >= ice::ptr_add(ptr, minfo.size * count)); + return { reinterpret_cast(ptr), count }; } } // namespace span diff --git a/source/code/core/collections/public/ice/string/impl/string.inl b/source/code/core/collections/public/ice/string/impl/string.inl index 66324722..4aa17b5e 100644 --- a/source/code/core/collections/public/ice/string/impl/string.inl +++ b/source/code/core/collections/public/ice/string/impl/string.inl @@ -180,9 +180,15 @@ namespace ice } template - constexpr auto starts_with(ice::BasicString str, ice::BasicString prefix) noexcept + constexpr auto substr(ice::BasicString str, ice::ref32 ref) noexcept -> ice::BasicString { - return ice::string::substr(str, 0, ice::string::size(prefix)) == prefix; + return ice::string::substr(str, ref.offset, ref.size); + } + + template + constexpr auto starts_with(ice::BasicString str, ice::concepts::StringType auto prefix) noexcept + { + return ice::string::substr(str, 0, prefix._size) == prefix; } @@ -384,6 +390,14 @@ namespace ice }; } + template + constexpr auto meminfo(ice::BasicString str) noexcept -> ice::meminfo + { + return ice::meminfo{ + ice::meminfo_of * ice::string::size(str) + }; + } + } // namespace string } // namespace ice diff --git a/source/code/core/collections/public/ice/string/string.hxx b/source/code/core/collections/public/ice/string/string.hxx index 6a6e9d88..92fd0852 100644 --- a/source/code/core/collections/public/ice/string/string.hxx +++ b/source/code/core/collections/public/ice/string/string.hxx @@ -41,7 +41,10 @@ namespace ice::string constexpr auto substr(ice::BasicString str, ice::ucount pos, ice::ucount len = ice::String_NPos) noexcept -> ice::BasicString; template - constexpr auto starts_with(ice::BasicString str, ice::BasicString prefix) noexcept; + constexpr auto substr(ice::BasicString str, ice::ref32 ref) noexcept -> ice::BasicString; + + template + constexpr auto starts_with(ice::BasicString str, ice::concepts::StringType auto prefix) noexcept; template constexpr auto find_first_of( @@ -109,6 +112,9 @@ namespace ice::string template constexpr auto data_view(ice::BasicString str) noexcept -> typename ice::Data; + template + constexpr auto meminfo(ice::BasicString str) noexcept -> ice::meminfo; + } // namespace ice::string namespace ice diff --git a/source/code/core/collections/public/ice/string_types.hxx b/source/code/core/collections/public/ice/string_types.hxx index c93b585c..50985f20 100644 --- a/source/code/core/collections/public/ice/string_types.hxx +++ b/source/code/core/collections/public/ice/string_types.hxx @@ -222,3 +222,15 @@ namespace ice } } // namespace ice + +namespace ice::concepts +{ + + template + concept StringType = std::is_same_v, Type> + || std::is_same_v, Type> + && requires(Type t) { + { t._size } -> std::convertible_to; + }; + +} // namespace ice::concepts diff --git a/source/code/core/core/public/ice/concept/pimpl_type.hxx b/source/code/core/core/public/ice/concept/pimpl_type.hxx new file mode 100644 index 00000000..8eff740f --- /dev/null +++ b/source/code/core/core/public/ice/concept/pimpl_type.hxx @@ -0,0 +1,54 @@ +#pragma once +#include + +namespace ice::concepts +{ + + enum class PimplFlags : ice::u8 + { + None = 0x0, + Default = None, + + NoMoveSemantics = 0x01, + NoCopySemantics = 0x02, + NoMoveCopySemantics = NoMoveSemantics | NoCopySemantics, + }; + + class PimplType + { + protected: + template + struct Internal; + + public: + template + PimplType(Internal* data) noexcept; + + // TODO: Move this somewhere else! + PimplType(PimplType&& other) noexcept = delete; + auto operator=(PimplType&& other) noexcept -> PimplType& = delete; + PimplType(PimplType const& other) noexcept = delete; + auto operator=(PimplType const& other) noexcept -> PimplType& = delete; + + protected: + template + auto internal(this Self& self) noexcept -> Internal&; + + private: + void* _internal; + }; + + + template + inline PimplType::PimplType(Internal* data) noexcept + : _internal{ data } + { + } + + template + auto PimplType::internal(this Self& self) noexcept -> Internal& + { + return *reinterpret_cast*>(self._internal); + } + +} // namespace ice::concepts diff --git a/source/code/core/core/public/ice/constants.hxx b/source/code/core/core/public/ice/constants.hxx index fb46126c..fad9d069 100644 --- a/source/code/core/core/public/ice/constants.hxx +++ b/source/code/core/core/public/ice/constants.hxx @@ -46,4 +46,16 @@ namespace ice constexpr ice::f32 const f32_nan = std::numeric_limits::signaling_NaN(); constexpr ice::f64 const f64_nan = std::numeric_limits::signaling_NaN(); + // Typed zero values + constexpr ice::f32 const f32_0 = ice::f32(0.0f); + constexpr ice::f64 const f64_0 = ice::f64(0.0); + constexpr ice::i8 const i8_0 = ice::i8(0); + constexpr ice::i16 const i16_0 = ice::i16(0); + constexpr ice::i32 const i32_0 = ice::i32(0); + constexpr ice::i64 const i64_0 = ice::i64(0); + constexpr ice::u8 const u8_0 = ice::u8(0); + constexpr ice::u16 const u16_0 = ice::u16(0); + constexpr ice::u32 const u32_0 = ice::u32(0); + constexpr ice::u64 const u64_0 = ice::u64(0); + } // namespace ice diff --git a/source/code/core/core/public/ice/error_codes.hxx b/source/code/core/core/public/ice/error_codes.hxx index b1f6c2e4..ecfabd6e 100644 --- a/source/code/core/core/public/ice/error_codes.hxx +++ b/source/code/core/core/public/ice/error_codes.hxx @@ -16,6 +16,9 @@ namespace ice static constexpr ice::ErrorCode E_InvalidArgument{ "E.0002:General:Invalid argument provided" }; static constexpr ice::ErrorCode E_OutOfRange{ "E.0003:General:Accessing value out of range" }; static constexpr ice::ErrorCode E_NotImplemented{ "E.0004:General:Function or method is not implemented" }; + static constexpr ice::ErrorCode E_NullPointer{ "E.0005:General:Passed '{nullptr}' to function expecting valid pointer." }; + static constexpr ice::ErrorCode E_NullPointerData{ "E.0006:General:Passed 'Data{nullptr}' object to function expecting valid data." }; + static constexpr ice::ErrorCode E_NullPointerMemory{ "E.0007:General:Passed 'Memory{nullptr}' object to function expecting valid memory." }; static constexpr ice::ErrorCode E_TaskCanceled{ "E.1001:Tasks:Task canceled" }; // Aliases are comparable diff --git a/source/code/core/core/public/ice/types.hxx b/source/code/core/core/public/ice/types.hxx index 5f035863..233a0f52 100644 --- a/source/code/core/core/public/ice/types.hxx +++ b/source/code/core/core/public/ice/types.hxx @@ -28,6 +28,30 @@ namespace ice using uptr = std::uintptr_t; + // Declaration of ref types + + //! \brief Holds 'offset' and 'size' fields (u32) to access data stored in a buffer-like object. + using ref32 = struct { ice::u32 offset; ice::u32 size; }; + + //! \brief Holds 'offset' and 'size' fields (u16) to access data stored in a buffer-like object. + struct ref16 + { + ice::u16 offset; + ice::u16 size; + + constexpr operator ice::ref32() const noexcept { return ref32{ offset, size }; } + }; + + //! \brief Holds 'offset' and 'size' fields (u8) to access data stored in a buffer-like object. + struct ref8 + { + ice::u8 offset; + ice::u8 size; + + constexpr operator ice::ref16() const noexcept { return ref16{ offset, size }; } + constexpr operator ice::ref32() const noexcept { return ref32{ offset, size }; } + }; + // Forward declaration of time-types struct Ts; struct Tms; diff --git a/source/code/core/math/public/ice/math.hxx b/source/code/core/math/public/ice/math.hxx index fd2a8f2c..9baacce8 100644 --- a/source/code/core/math/public/ice/math.hxx +++ b/source/code/core/math/public/ice/math.hxx @@ -31,4 +31,7 @@ namespace ice template<> constexpr inline ShardPayloadID Constant_ShardPayloadID = ice::shard_payloadid("ice::vec2i"); + template<> + constexpr inline ShardPayloadID Constant_ShardPayloadID = ice::shard_payloadid("ice::vec2f"); + } // namespace ice diff --git a/source/code/core/memsys/private/mem_allocator.cxx b/source/code/core/memsys/private/mem_allocator.cxx index 6ad89cf8..37ceef80 100644 --- a/source/code/core/memsys/private/mem_allocator.cxx +++ b/source/code/core/memsys/private/mem_allocator.cxx @@ -194,6 +194,7 @@ namespace ice AllocatorBase::~AllocatorBase() noexcept { + // TODO: Introduce low-level logger just for debug and dev builds. ICE_ASSERT_CORE(allocation_count() == 0); } diff --git a/source/code/core/modules/private/module_negotiator.cxx b/source/code/core/modules/private/module_negotiator.cxx index c0a66c62..2a763300 100644 --- a/source/code/core/modules/private/module_negotiator.cxx +++ b/source/code/core/modules/private/module_negotiator.cxx @@ -6,6 +6,11 @@ namespace ice { + bool ModuleNegotiatorBase::from_app() const noexcept + { + return negotiator_api->fn_is_app_context(negotiator_context); + } + bool ModuleNegotiatorBase::query_apis( ice::StringID_Arg api_name, ice::u32 api_version, diff --git a/source/code/core/modules/private/module_register.cxx b/source/code/core/modules/private/module_register.cxx index c136537e..faeba98f 100644 --- a/source/code/core/modules/private/module_register.cxx +++ b/source/code/core/modules/private/module_register.cxx @@ -29,7 +29,9 @@ namespace ice { DefaultModuleRegister* module_register; DefaultModuleEntry current_module; + bool in_app_context; + static bool from_app(ModuleNegotiatorAPIContext*) noexcept; static bool get_module_api(ModuleNegotiatorAPIContext*, ice::StringID_Hash, ice::u32, ice::ModuleAPI*) noexcept; static bool get_module_apis(ModuleNegotiatorAPIContext*, ice::StringID_Hash, ice::u32, ice::ModuleAPI*, ice::ucount*) noexcept; static bool register_module(ModuleNegotiatorAPIContext*, ice::StringID_Hash, FnModuleSelectAPI*) noexcept; @@ -49,7 +51,8 @@ namespace ice bool load_module( ice::Allocator& alloc, ice::FnModuleLoad* load_fn, - ice::FnModuleUnload* unload_fn + ice::FnModuleUnload* unload_fn, + bool from_shared_library ) noexcept override; auto api_count( @@ -117,7 +120,8 @@ namespace ice load_module( alloc, reinterpret_cast(load_proc), - reinterpret_cast(unload_proc) + reinterpret_cast(unload_proc), + /* is_app_context */ false ); ice::array::push_back(_module_handles, ice::move(module_handle)); @@ -130,7 +134,8 @@ namespace ice bool DefaultModuleRegister::load_module( ice::Allocator& alloc, ice::FnModuleLoad* load_fn, - ice::FnModuleUnload* unload_fn + ice::FnModuleUnload* unload_fn, + bool from_shared_library ) noexcept { DefaultModuleEntry module_entry{ @@ -142,9 +147,11 @@ namespace ice ModuleNegotiatorAPIContext negotiator_context{ .module_register = this, .current_module = module_entry, + .in_app_context = from_shared_library }; ModuleNegotiatorAPI negotiator{ + .fn_is_app_context = ModuleNegotiatorAPIContext::from_app, .fn_select_apis = ModuleNegotiatorAPIContext::get_module_apis, .fn_register_api = ModuleNegotiatorAPIContext::register_module, }; @@ -220,6 +227,11 @@ namespace ice return true; } + bool ModuleNegotiatorAPIContext::from_app(ModuleNegotiatorAPIContext* ctx) noexcept + { + return ctx->in_app_context; + } + bool ModuleNegotiatorAPIContext::get_module_apis( ice::ModuleNegotiatorAPIContext* ctx, ice::StringID_Hash api_name, diff --git a/source/code/core/modules/public/ice/module.hxx b/source/code/core/modules/public/ice/module.hxx index 3bc6f7ca..c93eb7c9 100644 --- a/source/code/core/modules/public/ice/module.hxx +++ b/source/code/core/modules/public/ice/module.hxx @@ -63,5 +63,6 @@ namespace ice #else # define IS_WORKAROUND_MODULE_INITIALIZATION(type) #endif +#define ICE_WORKAROUND_MODULE_INITIALIZATION(type) IS_WORKAROUND_MODULE_INITIALIZATION(type) } // namespace ice diff --git a/source/code/core/modules/public/ice/module_concepts.hxx b/source/code/core/modules/public/ice/module_concepts.hxx index 0bbcf736..d4c9d1ff 100644 --- a/source/code/core/modules/public/ice/module_concepts.hxx +++ b/source/code/core/modules/public/ice/module_concepts.hxx @@ -32,6 +32,7 @@ namespace ice::concepts concept ModuleNegotiator = requires(T const& t, ice::StringID_Arg id, ice::FnModuleSelectAPI* api) { { t.query_apis(id, ice::u32{}, (ice::ModuleAPI*) nullptr, (ice::ucount*)nullptr) } -> std::convertible_to; { t.register_api(id, api) } -> std::convertible_to; + { t.from_app() } -> std::convertible_to; } && requires(T const& t, ice::ProcAPIQuickRegisterFunc func) { { t.register_api(func) } -> std::convertible_to; }; diff --git a/source/code/core/modules/public/ice/module_negotiator.hxx b/source/code/core/modules/public/ice/module_negotiator.hxx index d1023d42..3558ba04 100644 --- a/source/code/core/modules/public/ice/module_negotiator.hxx +++ b/source/code/core/modules/public/ice/module_negotiator.hxx @@ -15,6 +15,12 @@ namespace ice //! \details Allows to register and query APIs independent if the module is dynamically or statically linked. struct ModuleNegotiatorAPI { + //! \returns 'true' if the module is loaded from a shared library. + //! Can be used to load system modules only from the executable. + using FnModuleInAppContext = bool(*)( + ice::ModuleNegotiatorAPIContext* + ) noexcept; + //! \brief Used to return API pointers into the given array. //! \note If the array pointer is null, the size pointer will be set to the required size. //! \note If the array is not large enough, the returned elements will be truncated without a specific order. @@ -34,6 +40,7 @@ namespace ice ice::FnModuleSelectAPI* fn_api_selector ) noexcept; + FnModuleInAppContext fn_is_app_context; FnModuleSelectAPIs fn_select_apis; FnModuleRegisterAPI fn_register_api; }; @@ -48,6 +55,9 @@ namespace ice ice::ModuleNegotiatorAPIContext* negotiator_context ) noexcept; + //! \returns 'true' when the module is loaded in the main application context. + bool from_app() const noexcept; + //! \copy ice::ModuleQuery::query_apis bool query_apis( ice::StringID_Arg api_name, diff --git a/source/code/core/modules/public/ice/module_register.hxx b/source/code/core/modules/public/ice/module_register.hxx index 4bda2ed1..eff77c4c 100644 --- a/source/code/core/modules/public/ice/module_register.hxx +++ b/source/code/core/modules/public/ice/module_register.hxx @@ -38,13 +38,14 @@ namespace ice virtual bool load_module( ice::Allocator& alloc, ice::FnModuleLoad* load_fn, - ice::FnModuleUnload* unload_fn + ice::FnModuleUnload* unload_fn, + bool from_shared_library ) noexcept = 0; //! \brief Loads a module using a module info structure. virtual bool load_module(ice::Allocator& alloc, ice::ModuleInfo const& module_info) noexcept { - return this->load_module(alloc, module_info.fn_load, module_info.fn_unload); + return this->load_module(alloc, module_info.fn_load, module_info.fn_unload, true); } //! \brief Loads a module using a module type. This is the preferred way of loading modules implicitly. diff --git a/source/code/core/utils/private/assert.cxx b/source/code/core/utils/private/assert.cxx index 916c1cd4..c66c6c23 100644 --- a/source/code/core/utils/private/assert.cxx +++ b/source/code/core/utils/private/assert.cxx @@ -56,7 +56,7 @@ namespace ice::detail header_buffer_raw, 128, LogFormat_AssertLineHeader, - fmt::localtime(std::time(nullptr)) + ice::detail::local_time() ); if (LogState::minimal_header_length < format_result.size) diff --git a/source/code/core/utils/private/log.cxx b/source/code/core/utils/private/log.cxx index 2a7d8058..12c8791c 100644 --- a/source/code/core/utils/private/log.cxx +++ b/source/code/core/utils/private/log.cxx @@ -92,7 +92,7 @@ namespace ice::detail header_buffer_raw, 256, fmt_string(LogFormat_LogLineHeader), - fmt::localtime(std::time(nullptr)), + ice::detail::local_time(), fmt_string(detail::severity_value[static_cast(severity)]), fmt_string(base_tag_name), fmt_string(ice::string::empty(tag_name) || ice::string::empty(base_tag_name) ? "" : " | "), diff --git a/source/code/core/utils/private/log_internal.hxx b/source/code/core/utils/private/log_internal.hxx index 1d022eaf..d44d4327 100644 --- a/source/code/core/utils/private/log_internal.hxx +++ b/source/code/core/utils/private/log_internal.hxx @@ -10,6 +10,7 @@ #include #include +#include namespace ice::detail { @@ -136,4 +137,20 @@ namespace ice::detail "DEBG", }; +#if ISP_WEBAPP || ISP_ANDROID || ISP_LINUX + auto local_time() noexcept -> std::tm + { + std::chrono::system_clock::time_point const now = std::chrono::system_clock::now(); + std::time_t const current_time = std::chrono::system_clock::to_time_t(now); + std::tm const* localtime = std::localtime(¤t_time); + return *localtime; + } +#else + auto local_time() noexcept + { + static auto const current_timezone = std::chrono::current_zone(); + return current_timezone->to_local(std::chrono::system_clock::now()); + } +#endif + } // namespace ice::detail diff --git a/source/code/core/utils/private/log_webasm.cxx b/source/code/core/utils/private/log_webasm.cxx index f70c4e5b..03b242e6 100644 --- a/source/code/core/utils/private/log_webasm.cxx +++ b/source/code/core/utils/private/log_webasm.cxx @@ -10,8 +10,8 @@ #include #include -#include #include +#include namespace ice::detail::webasm { @@ -74,7 +74,7 @@ namespace ice::detail::webasm header_buffer_raw, 128, LogFormat_AssertLineHeader, - fmt::localtime(std::time(nullptr)) + ice::detail::local_time() ); if (LogState::minimal_header_length < format_result.size) @@ -160,7 +160,7 @@ namespace ice::detail::webasm header_buffer_raw, 256, LogFormat_LogLineHeader, - fmt::localtime(std::time(nullptr)), + ice::detail::local_time(), fmt_string(detail::severity_value[static_cast(severity)]), fmt_string(base_tag_name), fmt_string(ice::string::empty(tag_name) || ice::string::empty(base_tag_name) ? "" : " | "), diff --git a/source/code/core/utils/private/string_utils.cxx b/source/code/core/utils/private/string_utils.cxx index 846a5b8d..f3c0d61d 100644 --- a/source/code/core/utils/private/string_utils.cxx +++ b/source/code/core/utils/private/string_utils.cxx @@ -8,6 +8,46 @@ namespace ice { + auto compare( + ice::String left, + ice::String right, + ice::u64 count, + ice::CaseSensitive check_case /*= CaseSensitive::No*/ + ) noexcept -> ice::CompareResult + { +#if ISP_WINDOWS + ice::u32 const comp_result = check_case == CaseSensitive::Yes + ? strncmp(ice::string::begin(left), ice::string::begin(right), count) + : strnicmp(ice::string::begin(left), ice::string::begin(right), count); +#elif ISP_UNIX + ice::u32 const comp_result = check_case == CaseSensitive::Yes + ? strncmp(ice::string::begin(left), ice::string::begin(right), count) + : strncasecmp(ice::string::begin(left), ice::string::begin(right), count); +#endif + return comp_result == 0 ? CompareResult::Equal : + (comp_result < 0 ? CompareResult::Smaller : CompareResult::Larger); + } + + auto compare( + ice::String left, + ice::String right, + ice::CaseSensitive check_case /*= CaseSensitive::No*/ + ) noexcept -> ice::CompareResult + { + ice::u32 const max_size = ice::min(ice::size(left), ice::size(right)); +#if ISP_WINDOWS + ice::u32 const comp_result = check_case == CaseSensitive::Yes + ? strncmp(ice::string::begin(left), ice::string::begin(right), max_size) + : strnicmp(ice::string::begin(left), ice::string::begin(right), max_size); +#elif ISP_UNIX + ice::u32 const comp_result = check_case == CaseSensitive::Yes + ? strncmp(ice::string::begin(left), ice::string::begin(right), max_size) + : strncasecmp(ice::string::begin(left), ice::string::begin(right), max_size); +#endif + return comp_result == 0 ? CompareResult::Equal : + (comp_result < 0 ? CompareResult::Smaller : CompareResult::Larger); + } + auto utf8_to_wide_size( ice::String utf8_str ) noexcept -> ice::u32 diff --git a/source/code/core/utils/public/ice/log_formatters.hxx b/source/code/core/utils/public/ice/log_formatters.hxx index 032c1da6..c698943e 100644 --- a/source/code/core/utils/public/ice/log_formatters.hxx +++ b/source/code/core/utils/public/ice/log_formatters.hxx @@ -160,6 +160,22 @@ struct fmt::formatter } }; +template<> +struct fmt::formatter : fmt::formatter +{ + template + constexpr auto format(ice::isize value, FormatContext& ctx) const noexcept + { + auto output = ctx.out(); + if (value.value < 0) + { + output = fmt::format_to(ctx.out(), "-"); + value = -value; + } + return fmt::formatter::format(value.to_usize(), ctx); + } +}; + template<> struct fmt::formatter { diff --git a/source/code/core/utils/public/ice/string_utils.hxx b/source/code/core/utils/public/ice/string_utils.hxx index 390ad17a..dbf50d23 100644 --- a/source/code/core/utils/public/ice/string_utils.hxx +++ b/source/code/core/utils/public/ice/string_utils.hxx @@ -32,6 +32,11 @@ namespace ice } // namespace string + enum class CaseSensitive : bool { No, Yes }; + enum class CompareResult : ice::i8 { Smaller = -1, Equal = 0, Larger = 1 }; + auto compare(ice::String left, ice::String right, ice::CaseSensitive = CaseSensitive::No) noexcept -> ice::CompareResult; + auto compare(ice::String left, ice::String right, ice::u64 count, ice::CaseSensitive = CaseSensitive::No) noexcept -> ice::CompareResult; + auto utf8_to_wide_size(ice::String path) noexcept -> ice::u32; bool utf8_to_wide_append(ice::String path, ice::HeapString& out_str) noexcept; auto utf8_to_wide(ice::Allocator& alloc, ice::String path) noexcept -> ice::HeapString; @@ -57,11 +62,34 @@ namespace ice auto from_chars(ice::String str, T& out_value) noexcept -> ice::FromCharsResult { ice::ErrorCode res = ice::S_Ok; - std::from_chars_result const fc_res = std::from_chars( - ice::string::begin(str), - ice::string::end(str), - out_value - ); + std::from_chars_result fc_res; + if constexpr (std::is_integral_v) + { + fc_res = std::from_chars( + ice::string::begin(str), + ice::string::end(str), + out_value + ); + } + else + { +#if ISP_COMPILER_CLANG <= 20 || ISP_WEBAPP || ISP_ANDROID + // Because Libc++ did not support from_chars for floats up until clang.20 we need to use the old C style approach... + // We don't try to handle errors in this version. + fc_res.ec = std::errc{}; + char* ptr_end = nullptr; // Why the hell is this a char ptr? + out_value = strtof(ice::string::begin(str), &ptr_end); + ICE_ASSERT_CORE(ice::ptr_distance(ice::string::begin(str), ptr_end).value <= ice::size(str)); + fc_res.ptr = ptr_end; +#else + fc_res = std::from_chars( + ice::string::begin(str), + ice::string::end(str), + out_value, + std::chars_format::general + ); +#endif + } if (fc_res.ec == std::errc::result_out_of_range) { @@ -83,11 +111,25 @@ namespace ice auto from_chars(char const* str_beg, char const* str_end, T& out_value) noexcept -> ice::FromCharsResult { ice::ErrorCode res = ice::S_Ok; - std::from_chars_result const fc_res = std::from_chars( - str_beg, - str_end, - out_value - ); + std::from_chars_result fc_res; + if constexpr (std::is_integral_v) + { + fc_res = std::from_chars(str_beg, str_end, out_value); + } + else + { +#if ISP_COMPILER_CLANG <= 20 || ISP_WEBAPP || ISP_ANDROID + // Because Libc++ did not support from_chars for floats up until clang.20 we need to use the old C style approach... + // We don't try to handle errors in this version. + fc_res.ec = std::errc{}; + char* ptr_end = nullptr; // Why the hell is this a char ptr? + out_value = strtof(str_beg, &ptr_end); + ICE_ASSERT_CORE(ice::ptr_distance(str_beg, ptr_end) <= ice::ptr_distance(str_beg, str_end)); + fc_res.ptr = ptr_end; +#else + fc_res = std::from_chars(str_beg, str_end, out_value, std::chars_format::general); +#endif + } if (fc_res.ec == std::errc::result_out_of_range) { diff --git a/source/code/framework/framework_base/private/framework_main.cxx b/source/code/framework/framework_base/private/framework_main.cxx index ba94bad5..8f1939bd 100644 --- a/source/code/framework/framework_base/private/framework_main.cxx +++ b/source/code/framework/framework_base/private/framework_main.cxx @@ -46,6 +46,7 @@ #include #include +#include #include #include @@ -435,6 +436,7 @@ auto ice_setup( ice::EngineCreateInfo engine_create_info{ .states = ice::create_state_tracker(state.alloc), .archetypes = ice::move(archetypes), + .action_stack = ice::create_input_action_stack(alloc, "") }; { ice::UniquePtr asset_categories = ice::create_asset_category_archive(state.engine_alloc); @@ -595,6 +597,9 @@ auto ice_game_frame( runtime.input_tracker->process_device_events(state.platform.core->input_events(), runtime.input_events); ice_process_input_events(runtime.input_events, new_frame->shards()); + state.engine->actions().process_inputs(runtime.input_events); + state.engine->actions().publish_shards(new_frame->shards()); + // Push pending world events state.game->on_update(*state.engine, *new_frame); state.engine->worlds().query_pending_events(new_frame->shards()); diff --git a/source/code/framework/framework_base/private/traits/ui/render_ui_trait.hxx b/source/code/framework/framework_base/private/traits/ui/render_ui_trait.hxx index a152121f..9ca34838 100644 --- a/source/code/framework/framework_base/private/traits/ui/render_ui_trait.hxx +++ b/source/code/framework/framework_base/private/traits/ui/render_ui_trait.hxx @@ -128,6 +128,6 @@ namespace ice } // namespace ice template<> -constexpr ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::RenderUIRequest const*"); +constexpr inline ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::RenderUIRequest const*"); #endif diff --git a/source/code/framework/framework_base/public/ice/game_render_traits.hxx b/source/code/framework/framework_base/public/ice/game_render_traits.hxx index 76348f7a..8c1091c4 100644 --- a/source/code/framework/framework_base/public/ice/game_render_traits.hxx +++ b/source/code/framework/framework_base/public/ice/game_render_traits.hxx @@ -95,7 +95,7 @@ namespace ice } // namespace ice template<> -constexpr ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::DebugDrawCommandList const*"); +constexpr inline ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::DebugDrawCommandList const*"); template<> -constexpr ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::DrawTextCommand const*"); +constexpr inline ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::DrawTextCommand const*"); diff --git a/source/code/framework/framework_base/public/ice/game_ui.hxx b/source/code/framework/framework_base/public/ice/game_ui.hxx index 4c97f5f2..bd734f54 100644 --- a/source/code/framework/framework_base/public/ice/game_ui.hxx +++ b/source/code/framework/framework_base/public/ice/game_ui.hxx @@ -45,4 +45,4 @@ namespace ice } // namespace ice template<> -constexpr ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::UpdateUIResource const*"); +constexpr inline ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::UpdateUIResource const*"); diff --git a/source/code/iceshard/engine/engine.bff b/source/code/iceshard/engine/engine.bff index d824e0a8..d56cffce 100644 --- a/source/code/iceshard/engine/engine.bff +++ b/source/code/iceshard/engine/engine.bff @@ -19,6 +19,7 @@ 'modules' 'platform' 'input_system' + 'input_action_system' 'render_system' } ] diff --git a/source/code/iceshard/engine/public/ice/engine.hxx b/source/code/iceshard/engine/public/ice/engine.hxx index 017cf404..c3bf4fdd 100644 --- a/source/code/iceshard/engine/public/ice/engine.hxx +++ b/source/code/iceshard/engine/public/ice/engine.hxx @@ -17,7 +17,7 @@ namespace ice ice::UniquePtr traits; ice::UniquePtr states; ice::UniquePtr archetypes; - + ice::UniquePtr action_stack; }; struct Engine @@ -32,6 +32,8 @@ namespace ice virtual auto entity_archetypes() noexcept -> ice::ecs::ArchetypeIndex& = 0; virtual auto entity_index() noexcept -> ice::ecs::EntityIndex& = 0; virtual auto entity_storage() noexcept -> ice::ecs::EntityStorage& = 0; + + virtual auto actions() noexcept -> ice::InputActionStack& = 0; }; } // namespace ice diff --git a/source/code/iceshard/engine/public/ice/engine_types.hxx b/source/code/iceshard/engine/public/ice/engine_types.hxx index 6d3971ef..ef666daf 100644 --- a/source/code/iceshard/engine/public/ice/engine_types.hxx +++ b/source/code/iceshard/engine/public/ice/engine_types.hxx @@ -8,6 +8,7 @@ #include #include #include +#include namespace ice { diff --git a/source/code/iceshard/iceshard/private/iceshard_engine.cxx b/source/code/iceshard/iceshard/private/iceshard_engine.cxx index 9d6c4c81..d8bb9f65 100644 --- a/source/code/iceshard/iceshard/private/iceshard_engine.cxx +++ b/source/code/iceshard/iceshard/private/iceshard_engine.cxx @@ -20,6 +20,7 @@ namespace ice , _states{ ice::move(create_info.states) } , _entity_archetypes{ ice::move(create_info.archetypes) } , _entity_storage{ _allocator, *_entity_archetypes } + , _inputactions{ ice::move(create_info.action_stack) } , _worlds{ _allocator, _entity_storage, ice::move(create_info.traits), *_states } { } diff --git a/source/code/iceshard/iceshard/private/iceshard_engine.hxx b/source/code/iceshard/iceshard/private/iceshard_engine.hxx index 7dbfcc98..c837c913 100644 --- a/source/code/iceshard/iceshard/private/iceshard_engine.hxx +++ b/source/code/iceshard/iceshard/private/iceshard_engine.hxx @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include "iceshard_world_manager.hxx" @@ -32,6 +34,8 @@ namespace ice auto entity_index() noexcept -> ice::ecs::EntityIndex& override { return _entity_storage.entities(); } auto entity_storage() noexcept -> ice::ecs::EntityStorage& override { return _entity_storage; } + auto actions() noexcept -> ice::InputActionStack& override { return *_inputactions; } + void destroy() noexcept; auto world_manager() noexcept -> ice::IceshardWorldManager& { return _worlds; } @@ -43,6 +47,7 @@ namespace ice ice::UniquePtr _states; ice::UniquePtr _entity_archetypes; ice::ecs::EntityStorage _entity_storage; + ice::UniquePtr _inputactions; ice::IceshardWorldManager _worlds; }; diff --git a/source/code/modules/imgui_module/private/imgui_system.cxx b/source/code/modules/imgui_module/private/imgui_system.cxx index 58054892..153f9747 100644 --- a/source/code/modules/imgui_module/private/imgui_system.cxx +++ b/source/code/modules/imgui_module/private/imgui_system.cxx @@ -2,6 +2,7 @@ /// SPDX-License-Identifier: MIT #include "imgui_system.hxx" +#include "imgui_trait.hxx" #include "widgets/imgui_allocator_tree.hxx" #include diff --git a/source/code/modules/imgui_module/private/imgui_system.hxx b/source/code/modules/imgui_module/private/imgui_system.hxx index f278ea2c..cbc1e2e6 100644 --- a/source/code/modules/imgui_module/private/imgui_system.hxx +++ b/source/code/modules/imgui_module/private/imgui_system.hxx @@ -15,14 +15,7 @@ namespace ice::devui { - struct ImGuiStats - { - ice::u32 draw_calls; - ice::u32 draw_vertices; - ice::u32 draw_indices; - ice::Tns draw_processtime; - ice::usize draw_datasize; - }; + struct ImGuiStats; class ImGuiWidgetFrame final : public ice::DevUIFrame { diff --git a/source/code/modules/imgui_module/private/imgui_trait.hxx b/source/code/modules/imgui_module/private/imgui_trait.hxx index bcd8a7f5..5bfbf367 100644 --- a/source/code/modules/imgui_module/private/imgui_trait.hxx +++ b/source/code/modules/imgui_module/private/imgui_trait.hxx @@ -16,6 +16,15 @@ namespace ice::devui class ImGuiSystem; + struct ImGuiStats + { + ice::u32 draw_calls; + ice::u32 draw_vertices; + ice::u32 draw_indices; + ice::Tns draw_processtime; + ice::usize draw_datasize; + }; + class ImGuiTrait final : public ice::Trait , public ice::TraitDevUI diff --git a/source/code/modules/shader_tools/private/shader_tools.cxx b/source/code/modules/shader_tools/private/shader_tools.cxx index 67629fa5..96c9ae00 100644 --- a/source/code/modules/shader_tools/private/shader_tools.cxx +++ b/source/code/modules/shader_tools/private/shader_tools.cxx @@ -46,7 +46,7 @@ namespace ice auto compiler_supported_shader_resources( ice::Span params - ) noexcept -> ice::Span + ) noexcept -> ice::Span { if constexpr (ice::build::is_windows) { @@ -144,6 +144,7 @@ namespace ice static void v1_compiler_api(ice::api::resource_compiler::v1::ResourceCompilerAPI& api) noexcept { #if ISP_WINDOWS || ISP_LINUX || ISP_WEBAPP + api.id_category = "ice/shader-resource"_sid; api.fn_prepare_context = compiler_context_prepare; api.fn_cleanup_context = compiler_context_cleanup; api.fn_supported_resources = compiler_supported_shader_resources; diff --git a/source/code/modules/vulkan_renderer/private/vk_allocator.cxx b/source/code/modules/vulkan_renderer/private/vk_allocator.cxx index e43e1a6b..9ee2fc0c 100644 --- a/source/code/modules/vulkan_renderer/private/vk_allocator.cxx +++ b/source/code/modules/vulkan_renderer/private/vk_allocator.cxx @@ -233,7 +233,11 @@ namespace ice::render::vk auto VulkanAllocator::vulkan_callbacks() const noexcept -> VkAllocationCallbacks const* { +#if 0 return &_vulkan_callbacks; +#else + return nullptr; +#endif } } // namespace ice::render::vk diff --git a/source/code/modules/vulkan_renderer/private/vk_shader_asset.cxx b/source/code/modules/vulkan_renderer/private/vk_shader_asset.cxx index 2975957e..947be154 100644 --- a/source/code/modules/vulkan_renderer/private/vk_shader_asset.cxx +++ b/source/code/modules/vulkan_renderer/private/vk_shader_asset.cxx @@ -57,7 +57,7 @@ namespace ice::render::vk ) noexcept { #if ISP_WINDOWS || ISP_LINUX - static ice::String constexpr extensions[]{ ".asl", ".glsl",".spv" }; + static ice::String constexpr extensions[]{ ".asl", ".glsl", ".spv" }; #else static ice::String constexpr extensions[]{ ".spv" }; #endif @@ -69,9 +69,22 @@ namespace ice::render::vk }; #if ISP_WINDOWS || ISP_LINUX - ice::ResourceCompiler compiler{ }; - module_query.query_api(compiler); - asset_category_archive.register_category(ice::render::AssetCategory_Shader, definition, &compiler); + static ice::HostAllocator host_alloc; + + ice::ResourceCompiler const* selected_compiler = nullptr; + ice::Array compilers{ host_alloc }; + module_query.query_apis(compilers); + + for (ice::ResourceCompiler const& compiler : compilers) + { + if (compiler.id_category == "ice/shader-resource"_sid) + { + selected_compiler = ice::addressof(compiler); + break; + } + } + + asset_category_archive.register_category(ice::render::AssetCategory_Shader, definition, selected_compiler); #else asset_category_archive.register_category(ice::render::AssetCategory_Shader, definition, nullptr); #endif diff --git a/source/code/platforms/platform_linux/private/linux_sdl2_utils.cxx b/source/code/platforms/platform_linux/private/linux_sdl2_utils.cxx index 4b97f42b..3b2db9f7 100644 --- a/source/code/platforms/platform_linux/private/linux_sdl2_utils.cxx +++ b/source/code/platforms/platform_linux/private/linux_sdl2_utils.cxx @@ -104,58 +104,59 @@ namespace ice::platform::linux::sdl2 return KeyboardKey::Unknown; } - auto map_sdl_mod_scancode(SDL_Scancode scancode) noexcept -> ice::input::KeyboardMod + auto map_sdl_mod_scancode(SDL_Scancode scancode) noexcept -> std::tuple { using ice::input::KeyboardMod; + using ice::input::KeyboardKey; if (scancode == SDL_Scancode::SDL_SCANCODE_LCTRL) { - return KeyboardMod::CtrlLeft; + return { KeyboardMod::CtrlLeft, KeyboardKey::KeyLeftCtrl }; } if (scancode == SDL_Scancode::SDL_SCANCODE_LSHIFT) { - return KeyboardMod::ShiftLeft; + return { KeyboardMod::ShiftLeft, KeyboardKey::KeyLeftShift }; } if (scancode == SDL_Scancode::SDL_SCANCODE_LALT) { - return KeyboardMod::AltLeft; + return { KeyboardMod::AltLeft, KeyboardKey::KeyLeftAlt }; } if (scancode == SDL_Scancode::SDL_SCANCODE_LGUI) { - return KeyboardMod::GuiLeft; + return { KeyboardMod::GuiLeft, KeyboardKey::KeyLeftGui }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RCTRL) { - return KeyboardMod::CtrlRight; + return { KeyboardMod::CtrlRight, KeyboardKey::KeyRightCtrl }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RSHIFT) { - return KeyboardMod::ShiftRight; + return { KeyboardMod::ShiftRight, KeyboardKey::KeyRightShift }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RALT) { - return KeyboardMod::AltRight; + return { KeyboardMod::AltRight, KeyboardKey::KeyRightAlt }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RGUI) { - return KeyboardMod::GuiRight; + return { KeyboardMod::GuiRight, KeyboardKey::KeyRightGui }; } if (scancode == SDL_Scancode::SDL_SCANCODE_MODE) { - return KeyboardMod::Mode; + return { KeyboardMod::Mode, KeyboardKey::KeyMode }; } if (scancode == SDL_Scancode::SDL_SCANCODE_NUMLOCKCLEAR) { - return KeyboardMod::NumLock; + return { KeyboardMod::NumLock, KeyboardKey::NumPadNumlockClear }; } if (scancode == SDL_Scancode::SDL_SCANCODE_CAPSLOCK) { - return KeyboardMod::CapsLock; + return { KeyboardMod::CapsLock, KeyboardKey::KeyCapsLock }; } - return KeyboardMod::None; + return { KeyboardMod::None, KeyboardKey::Unknown }; } @@ -252,11 +253,16 @@ namespace ice::platform::linux::sdl2 } else { - KeyboardMod const mod = map_sdl_mod_scancode(sdl_event.key.keysym.scancode); + auto const [mod, modkey] = map_sdl_mod_scancode(sdl_event.key.keysym.scancode); if (mod != KeyboardMod::None) { if (sdl_event.key.type == SDL_KEYDOWN) { + input_queue.push( + device, + DeviceMessage::KeyboardButtonDown, + modkey + ); input_queue.push( device, DeviceMessage::KeyboardModifierDown, @@ -265,6 +271,11 @@ namespace ice::platform::linux::sdl2 } else if (sdl_event.key.type == SDL_KEYUP) { + input_queue.push( + device, + DeviceMessage::KeyboardButtonUp, + modkey + ); input_queue.push( device, DeviceMessage::KeyboardModifierUp, diff --git a/source/code/platforms/platform_win32/private/win32_sdl2_utils.cxx b/source/code/platforms/platform_win32/private/win32_sdl2_utils.cxx index cbef56f5..fd84610e 100644 --- a/source/code/platforms/platform_win32/private/win32_sdl2_utils.cxx +++ b/source/code/platforms/platform_win32/private/win32_sdl2_utils.cxx @@ -104,58 +104,59 @@ namespace ice::platform::win32::sdl2 return KeyboardKey::Unknown; } - auto map_sdl_mod_scancode(SDL_Scancode scancode) noexcept -> ice::input::KeyboardMod + auto map_sdl_mod_scancode(SDL_Scancode scancode) noexcept -> std::tuple { using ice::input::KeyboardMod; + using ice::input::KeyboardKey; if (scancode == SDL_Scancode::SDL_SCANCODE_LCTRL) { - return KeyboardMod::CtrlLeft; + return { KeyboardMod::CtrlLeft, KeyboardKey::KeyLeftCtrl }; } if (scancode == SDL_Scancode::SDL_SCANCODE_LSHIFT) { - return KeyboardMod::ShiftLeft; + return { KeyboardMod::ShiftLeft, KeyboardKey::KeyLeftShift }; } if (scancode == SDL_Scancode::SDL_SCANCODE_LALT) { - return KeyboardMod::AltLeft; + return { KeyboardMod::AltLeft, KeyboardKey::KeyLeftAlt }; } if (scancode == SDL_Scancode::SDL_SCANCODE_LGUI) { - return KeyboardMod::GuiLeft; + return { KeyboardMod::GuiLeft, KeyboardKey::KeyLeftGui }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RCTRL) { - return KeyboardMod::CtrlRight; + return { KeyboardMod::CtrlRight, KeyboardKey::KeyRightCtrl }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RSHIFT) { - return KeyboardMod::ShiftRight; + return { KeyboardMod::ShiftRight, KeyboardKey::KeyRightShift }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RALT) { - return KeyboardMod::AltRight; + return { KeyboardMod::AltRight, KeyboardKey::KeyRightAlt }; } if (scancode == SDL_Scancode::SDL_SCANCODE_RGUI) { - return KeyboardMod::GuiRight; + return { KeyboardMod::GuiRight, KeyboardKey::KeyRightGui }; } if (scancode == SDL_Scancode::SDL_SCANCODE_MODE) { - return KeyboardMod::Mode; + return { KeyboardMod::Mode, KeyboardKey::KeyMode }; } if (scancode == SDL_Scancode::SDL_SCANCODE_NUMLOCKCLEAR) { - return KeyboardMod::NumLock; + return { KeyboardMod::NumLock, KeyboardKey::NumPadNumlockClear }; } if (scancode == SDL_Scancode::SDL_SCANCODE_CAPSLOCK) { - return KeyboardMod::CapsLock; + return { KeyboardMod::CapsLock, KeyboardKey::KeyCapsLock }; } - return KeyboardMod::None; + return { KeyboardMod::None, KeyboardKey::Unknown }; } @@ -252,11 +253,16 @@ namespace ice::platform::win32::sdl2 } else { - KeyboardMod const mod = map_sdl_mod_scancode(sdl_event.key.keysym.scancode); + auto const[mod, modkey] = map_sdl_mod_scancode(sdl_event.key.keysym.scancode); if (mod != KeyboardMod::None) { if (sdl_event.key.type == SDL_KEYDOWN) { + input_queue.push( + device, + DeviceMessage::KeyboardButtonDown, + modkey + ); input_queue.push( device, DeviceMessage::KeyboardModifierDown, @@ -265,6 +271,11 @@ namespace ice::platform::win32::sdl2 } else if (sdl_event.key.type == SDL_KEYUP) { + input_queue.push( + device, + DeviceMessage::KeyboardButtonUp, + modkey + ); input_queue.push( device, DeviceMessage::KeyboardModifierUp, diff --git a/source/code/projects.bff b/source/code/projects.bff index aacae4c3..4f035abf 100644 --- a/source/code/projects.bff +++ b/source/code/projects.bff @@ -17,6 +17,7 @@ #include "systems/asset_system/asset_system.bff" #include "systems/render_system/render_system.bff" #include "systems/input_system/input_system.bff" +#include "systems/input_action_system/input_action_system.bff" #include "systems/font_system/font_system.bff" #include "systems/ui_system/ui_system.bff" diff --git a/source/code/systems/input_action_system/input_action_system.bff b/source/code/systems/input_action_system/input_action_system.bff new file mode 100644 index 00000000..a4de5af6 --- /dev/null +++ b/source/code/systems/input_action_system/input_action_system.bff @@ -0,0 +1,31 @@ +/// Copyright 2021 - 2023, Dandielo +/// SPDX-License-Identifier: MIT + +.Project = +[ + .Name = 'input_action_system' + .Kind = .Kind_StaticLib + .Group = 'Systems' + + .BaseDir = '$WorkspaceCodeDir$/systems/input_action_system' + + .Public = + [ + .Modules = { + 'arctic' + } + .Uses = { + 'utils' + 'input_system' + 'asset_system' + } + ] + + .Private = + [ + .Modules = { + 'arctic' + } + ] +] +.Projects + .Project diff --git a/source/code/systems/input_action_system/private/input_action.cxx b/source/code/systems/input_action_system/private/input_action.cxx new file mode 100644 index 00000000..6c58832d --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action.cxx @@ -0,0 +1,65 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace ice +{ + + void asset_category_shader_definition( + ice::AssetCategoryArchive& asset_category_archive, + ice::ModuleQuery const& module_query + ) noexcept + { + static ice::String ext[]{ ".ias" }; + static ice::AssetCategoryDefinition const definition{ + .resource_extensions = ext + }; + asset_category_archive.register_category(ice::AssetCategory_InputActionsScript, definition); + } + + struct InputActionsModule : public ice::Module + { + +#if 0 + auto ias_compiler_supported_resources( + ice::Span params + ) noexcept -> ice::Span + { + static ice::String ext[]{ ".ias" }; + return ext; + } + + static void v1_compiler_api(ice::api::resource_compiler::v1::ResourceCompilerAPI& api) noexcept + { + api.id_category = "ice/ias-script-resource"_sid; + api.fn_supported_resources = ias_compiler_supported_resources; + } +#endif + + static void v1_archive_api(ice::detail::asset_system::v1::AssetArchiveAPI& api) noexcept + { + api.fn_register_categories = asset_category_shader_definition; + } + + static bool on_load(ice::Allocator& alloc, ice::ModuleNegotiator auto& negotiator) noexcept + { + // Since we are on the 'system' layer, we can be part of multiple dynamic libararies, and prefere to not be loaded from them. + if (negotiator.from_app()) + { +#if 0 // Currently we don't really support compiling for InputAction scripts. + negotiator.register_api(v1_compiler_api); +#endif + negotiator.register_api(v1_archive_api); + } + return true; + } + + ICE_WORKAROUND_MODULE_INITIALIZATION(InputActionsModule); + }; + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_executor.cxx b/source/code/systems/input_action_system/private/input_action_executor.cxx new file mode 100644 index 00000000..13523aea --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_executor.cxx @@ -0,0 +1,151 @@ +#include +#include +#include +#include +#include + +namespace ice +{ + + auto constant_at(ice::InputActionConstant id, ice::Span values) noexcept -> ice::f32 + { + return values[ice::u32(id)]; + } + + bool InputActionExecutor::execute_condition( + ice::InputActionCondition condition, + ice::InputActionRuntime const& action, + ice::f32 param + ) const noexcept + { + switch (condition) + { + using enum InputActionCondition; + case ActionToggleActive: return action.toggle_enabled; + case ActionToggleInactive: return action.toggle_enabled == false; + case AlwaysTrue: return true; + default: ICE_ASSERT_CORE(false); return false; + } + } + + void InputActionExecutor::prepare_constants( + ice::InputActionLayer const& layer + ) noexcept + { + layer.load_constants(_constants); + } + + bool InputActionExecutor::execute_condition( + ice::InputActionCondition condition, + ice::InputActionSource const& val, + ice::f32 param + ) const noexcept + { + ice::f32 const deadzone_cutoff = constant_at(InputActionConstant::ControllerAxisDeadzone, _constants); + + switch (condition) + { + using enum InputActionCondition; + // Source conditions + case Active: return val.event != InputActionSourceEvent::None; + case Pressed: return val.event == InputActionSourceEvent::KeyPress; + case Released: return val.event == InputActionSourceEvent::KeyRelease; + case Trigger: return val.event == InputActionSourceEvent::Trigger; + // Deadzone comparisons + case Axis: return val.value >= deadzone_cutoff; + case AxisDeadzone: return val.value < deadzone_cutoff; + // Source and Parameter comparisons + case Equal: return val.value == param; + case NotEqual: return val.value != param; + case Greater: return val.value > param; + case GreaterOrEqual: return val.value >= param; + case Lower: return val.value < param; + case LowerOrEqual: return val.value <= param; + // Action conditions + + default: + break; + } + ICE_ASSERT_CORE(false); + return false; + } + + void InputActionExecutor::execute_step( + ice::InputActionStep step, + ice::InputActionRuntime& runtime + ) const noexcept + { + switch (step) + { + using enum InputActionStep; + case Activate: + runtime.state = runtime.state * 2 + 1; + runtime.active = true; + break; + case Deactivate: + runtime.state = 0; + runtime.active = false; + runtime.toggle_enabled = false; + break; + case Toggle: + runtime.state = runtime.state * 2 + 1; + if (runtime.state == 1) + { + runtime.toggle_enabled = !runtime.toggle_enabled; + } + break; + case Reset: + runtime.value = ice::vec2f{}; + runtime.raw_value = ice::vec3f{}; + break; + case Time: + runtime.raw_value.x = (float) Ts(ice::clock::elapsed(runtime.timestamp, ice::clock::now())).value; + break; + default: + break; + } + } + + void InputActionExecutor::execute_step( + ice::InputActionStep step, + ice::InputActionSource const& source_value, + ice::f32& dst + ) const noexcept + { + ice::f32 const src = source_value.value; + + switch (step) + { + using enum InputActionStep; + case Set: dst = src; break; + case Add: dst += src; break; + case Sub: dst -= src; break; + default: + break; + } + } + + void InputActionExecutor::execute_modifier( + ice::InputActionModifier modifier, + ice::f32& action_value, + ice::f32 param + ) const noexcept + { + switch(modifier) + { + using enum InputActionModifier; + case Add: action_value += param; + case Sub: action_value -= param; + case Mul: action_value *= param; + case Div: + ICE_ASSERT_CORE(param != 0.0f); + action_value /= param; + break; + case MaxOf: action_value = ice::max(action_value, param); break; + case MinOf: action_value = ice::min(action_value, param); break; + default: + break; + } + } + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_internal_types.hxx b/source/code/systems/input_action_system/private/input_action_internal_types.hxx new file mode 100644 index 00000000..24831a3b --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_internal_types.hxx @@ -0,0 +1,65 @@ +#pragma once +#include +#include + +namespace ice +{ + + //! \brief Header-like structure for binarized version of an InputActionLayer. + //! \note This structure will be used for storing input actions in binary format for release builds. + struct InputActionLayerInfoHeader + { + ice::u8 size_name; + ice::u8 count_constants; + ice::u16 count_sources; + ice::u16 count_actions; + ice::u16 count_conditions; + ice::u16 count_steps; + ice::u16 count_modifiers; + ice::u32 offset_strings; + }; + + struct InputActionIndex + { + static constexpr ice::u16 SelfIndex = 8191; + + ice::u16 source_index : 13; + ice::u16 source_axis : 3; + }; + + static_assert(sizeof(ice::InputActionIndex) == sizeof(ice::u16)); + + struct InputActionConditionData + { + ice::InputActionIndex source; + ice::InputActionCondition id; + ice::InputActionConditionFlags flags; + ice::ref16 steps; + ice::f32 param; + }; + + static_assert(sizeof(InputActionConditionData) % 4 == 0); + + struct InputActionStepData + { + ice::InputActionIndex source; + ice::InputActionStep id; + ice::u8 dst_axis; + }; + + static_assert(sizeof(InputActionStepData) % 4 == 0); + + struct InputActionModifierData + { + // \brief The modifier to be applied to the final values stored in the action. + ice::InputActionModifier id; + + ice::u8 axis; // 0b0000'0[111] (0000'0[zyx]) + + //! \brief Offset where additional parameter are stored for float conditions. + ice::f32 param; + }; + + static_assert(sizeof(InputActionModifierData) % 4 == 0); + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_layer.cxx b/source/code/systems/input_action_system/private/input_action_layer.cxx new file mode 100644 index 00000000..4eaf8c85 --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_layer.cxx @@ -0,0 +1,498 @@ + +#include "input_action_internal_types.hxx" +#include "input_action_script_parser.hxx" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ice +{ + + struct InputActionLayerInfo + { + ice::String name; + ice::Span sources; + ice::Span actions; + ice::Span conditions; + ice::Span steps; + ice::Span modifiers; + ice::Span constants; + ice::Span constant_values; + ice::String strings; + }; + + template + auto load_field_from_data(ice::Span& out_span, ice::Data data, ice::usize offset, ice::ucount count) noexcept + { + out_span = ice::span::from_data(data, count, offset); + return ice::span::data_view(out_span).size; + } + + auto load_from_data(ice::Data data) noexcept -> ice::Expected + { + if (data.location == nullptr) + { + return E_NullPointerData; + } + + ice::InputActionLayerInfoHeader const& header = *reinterpret_cast(data.location); + ice::usize offset = ice::size_of; + + ice::InputActionLayerInfo result{}; + offset += load_field_from_data(result.sources, data, offset, header.count_sources); + offset += load_field_from_data(result.actions, data, offset, header.count_actions); + offset += load_field_from_data(result.conditions, data, offset, header.count_conditions); + offset += load_field_from_data(result.steps, data, offset, header.count_steps); + offset += load_field_from_data(result.modifiers, data, offset, header.count_modifiers); + offset += load_field_from_data(result.constant_values, data, offset, header.count_constants); + offset += load_field_from_data(result.constants, data, offset, header.count_constants); + + ICE_ASSERT_CORE(offset == ice::usize{ header.offset_strings }); + result.strings = ice::string::from_data( + data, + ice::usize{ header.offset_strings }, + ice::ucount( data.size.value - header.offset_strings ) + ); + result.name = ice::string::substr(result.strings, 0, header.size_name); + return result; + } + + class StandardInputActionLayer final : public ice::InputActionLayer + { + public: + StandardInputActionLayer( + ice::Allocator& alloc, + ice::Memory memory, + ice::InputActionLayerInfo const& info + ) noexcept + : _allocator{ alloc } + , _rawdata{ memory } + , _name{ info.name } + , _sources{ info.sources } + , _actions{ info.actions } + , _conditions{ info.conditions } + , _steps{ info.steps } + , _modifiers{ info.modifiers } + , _constants{ info.constants } + , _constant_values{ info.constant_values } + , _strings{ info.strings } + { } + + ~StandardInputActionLayer() noexcept override + { + _allocator.deallocate(_rawdata); + } + + auto name() const noexcept -> ice::String override + { + return _name; + } + + auto sources() const noexcept -> ice::Span override + { + return _sources; + } + + auto source_name(ice::InputActionSourceInputInfo const& source) const noexcept -> ice::String override + { + return ice::string::substr(_strings, source.name); + } + + auto actions() const noexcept -> ice::Span override + { + return _actions; + } + + auto action_name(ice::InputActionInfo const& action) const noexcept -> ice::String override + { + return ice::string::substr(_strings, action.name); + } + + auto load_constants(ice::Span constants_span) const noexcept -> ice::ucount override + { + ICE_ASSERT_CORE(ice::count(constants_span) >= Constant_CountInputActionConstants); + for (ice::InputActionConstantInfo const constant : _constants) + { + ice::u32 const idx = ice::u32(constant.identifier); + constants_span[idx] = _constant_values[constant.offset]; + } + return ice::count(_constants); + } + + auto process_inputs( + ice::Span input_events, + ice::Span source_values + ) const noexcept -> ice::ucount override + { + IPT_ZONE_SCOPED; + + // helpers + static auto comp_event_id = [](ice::input::InputEvent ev, ice::input::InputID id) noexcept -> bool + { + return ev.identifier == id; + }; + + ice::ucount count_processed = 0; + ice::ucount const count_events = ice::count(input_events); + + // Reset the temporary events. + for (ice::InputActionSourceInputInfo const& src : _sources) + { + ice::InputActionSource* const values = source_values[src.storage_offset]; + ice::u32 const count_values = 1 + ice::u32(src.type == InputActionSourceType::Axis2d); + + for (ice::u32 idx = 0; idx < count_values; ++idx) + { + values[idx].temp_event = InputActionSourceEvent::None; + } + } + + for (ice::InputActionSourceInputInfo const& src : _sources) + { + ice::InputActionSource* const values = source_values[src.storage_offset]; + ice::u32 const count_values = 1 + ice::u32(src.type == InputActionSourceType::Axis2d); + + ice::ucount event_index = 0; + if (ice::search(input_events, src.input, comp_event_id, event_index) == false) + { + // Reset any event that was a key-release event previously + for (ice::u32 idx = 0; idx < count_values; ++idx) + { + if (values[idx].event == InputActionSourceEvent::KeyRelease) + { + values[idx].event = InputActionSourceEvent::None; + } + } + continue; + } + + // We consume the event by placing the current last event at it's index. + count_processed += 1; + ice::input::InputEvent const ev = ice::exchange( + input_events[event_index], + input_events[count_events - count_processed] + ); + + // The actual value that is being updated + ICE_ASSERT_CORE(ev.axis_idx < count_values); + InputActionSource& value = values[ev.axis_idx]; + + if (ev.value_type == ice::input::InputValueType::Trigger) + { + value = { ev.value.axis.value_f32, InputActionSourceEvent::Trigger }; + } + else if (ev.value_type == ice::input::InputValueType::AxisInt) + { + value = { ice::f32(ev.value.axis.value_i32), InputActionSourceEvent::Axis }; + } + else if (ev.value_type == ice::input::InputValueType::AxisFloat) + { + // Check for deadzone values + if (src.param < ev.value.axis.value_f32) + { + value = { ev.value.axis.value_f32, InputActionSourceEvent::Axis }; + } + else + { + value = { ev.value.axis.value_f32, InputActionSourceEvent::AxisDeadzone }; + } + } + else if (value.temp_event != InputActionSourceEvent::KeyPress) + { + value = { + ev.value.button.state.released + ? 0.0f : 1.0f, + ev.value.button.state.released + ? InputActionSourceEvent::KeyRelease + : InputActionSourceEvent::KeyPress, + }; + } + } + + // Select the final events. + for (ice::InputActionSourceInputInfo const& src : _sources) + { + ice::InputActionSource* const values = source_values[src.storage_offset]; + ice::u32 const count_values = 1 + ice::u32(src.type == InputActionSourceType::Axis2d); + + for (ice::u32 idx = 0; idx < count_values; ++idx) + { + values[idx].event = values[idx].temp_event; + } + } + + // Clear the end of the events list + for (ice::u32 idx = 1; idx <= count_processed; ++idx) + { + input_events[count_events - idx] = ice::input::InputEvent{}; + } + return count_processed; + } + + bool update_actions( + ice::InputActionExecutor const& executor, + ice::Span source_values, + ice::HashMap& actions + ) const noexcept override + { + IPT_ZONE_SCOPED; + using enum ice::InputActionConditionFlags; + + for (ice::InputActionInfo const& action : _actions) + { + ice::String const action_name = ice::string::substr(_strings, action.name); + + ice::InputActionRuntime* const runtime = ice::hashmap::try_get(actions, ice::hash(action_name)); + // TODO: Check if we need this + //if (action.behavior != InputActionBehavior::Accumulated) + { + runtime->raw_value = {}; + } + + bool series_success = false; + ice::Span const conditions = ice::span::subspan(_conditions, action.conditions); + for (ice::InputActionConditionData const& cond : conditions) + { + bool cond_result = false; + if (cond.id >= ice::InputActionCondition::ActionEnabled) + { + ice::InputActionRuntime const* checked_action = runtime; // Seft (by-default) + if (cond.source.source_index != InputActionIndex::SelfIndex) + { + ice::InputActionInfo const checked_action_info = _actions[cond.source.source_index]; + ice::String const checked_action_name = ice::string::substr(_strings, checked_action_info.name); + checked_action = ice::hashmap::try_get(actions, ice::hash(checked_action_name)); + } + ICE_ASSERT_CORE(checked_action != nullptr); + cond_result = executor.execute_condition( + cond.id, + *checked_action, + cond.param + ); + } + else + { + ICE_ASSERT_CORE(source_values[cond.source.source_index] != nullptr); + ice::InputActionSource& value = source_values[cond.source.source_index][0]; + + if (value.event != InputActionSourceEvent::None) + { + cond_result = executor.execute_condition(cond.id, value, cond.param); + } + } + + // Can't check for 'SeriesOr' since it's just a zero value + if (ice::has_all(cond.flags, InputActionConditionFlags::SeriesAnd)) + { + series_success &= cond_result; + } + else + { + series_success |= cond_result; + } + + bool const check_success = ice::has_all(cond.flags, SeriesCheck) + ? series_success + : cond_result; + + if (ice::has_all(cond.flags, RunSteps) && check_success) + { + ice::Span const steps = ice::span::subspan(_steps, cond.steps); + for (ice::InputActionStepData const& step : steps) + { + if (step.id < InputActionStep::Set) + { + executor.execute_step(step.id, *runtime); + } + else + { + ICE_ASSERT_CORE(source_values[step.source.source_index] != nullptr); + ice::InputActionSource& value = source_values[step.source.source_index][step.source.source_axis]; + + executor.execute_step(step.id, value, runtime->raw_value.v[0][step.dst_axis]); + } + } + } + + // If this is not 'series finish', continue + if (ice::has_none(cond.flags, SeriesFinish)) + { + continue; + } + + // // If the condition should activate/deactivate implicitly + // // TODO: Move before the final check to allow activating on regular checks? + // if (ice::has_all(cond.flags, Activate) && runtime->enabled) + // { + // // This small operation allows use to implement 'Once' logic. + // // Initial state == 1, unless deactivated from now on we will always be above 1 + // // since we purposefully loosing bits due to overflow but always set the MSB again. + // runtime->state = runtime->state * 2 + 1; + // runtime->active = check_success; + // } + // else if (ice::has_all(cond.flags, Deactivate)) + // { + // runtime->state = 0; + // runtime->active = false; + // } + + // If the series is not successful, just continue + if (series_success == false || runtime->enabled == false) + { + // If the action finaly fails, we reset the state so we can again start counting from 0 + runtime->state = 0; + runtime->active = false; + continue; + } + + series_success = false; + + // continue, if it's not a final condition + if (ice::has_none(cond.flags, Final)) + { + continue; + } + + // Stop checking futher conditions, we are in the final condition + break; + } + } + + for (ice::InputActionInfo const& action : _actions) + { + ice::String const action_name = ice::string::substr(_strings, action.name); + ice::InputActionRuntime* const runtime = ice::hashmap::try_get(actions, ice::hash(action_name)); + + // Handles 'Toggle'. We only activate of the first press, which is `state == 1`. + if (action.behavior == InputActionBehavior::Toggled) + { + runtime->active |= runtime->toggle_enabled; + //if (runtime->state == 1) + //{ + // runtime->toggle_enabled = !runtime->toggle_enabled; + //} + //else + //{ + //} + } + // Handles 'Once'. Once state is bigger than `1` we always deactivate the action. + else if (action.behavior == InputActionBehavior::ActiveOnce && runtime->state > 1) + { + runtime->active = false; + } + + if (runtime->active == false) + { + runtime->was_active = false; + continue; + } + + if (runtime->was_active == false) + { + runtime->was_active = true; + runtime->timestamp = ice::clock::now(); + } + + // Update the final value and run modifiers over it. + runtime->value = { runtime->raw_value.x, runtime->raw_value.y }; + + ice::Span const mods = ice::span::subspan(_modifiers, action.mods); + for (ice::InputActionModifierData const& mod : mods) + { + executor.execute_modifier(mod.id, runtime->value.v[0][mod.axis], mod.param); + } + } + return false; + } + + private: + ice::Allocator& _allocator; + ice::Memory _rawdata; + + ice::String _name; + ice::Span _sources; + ice::Span _actions; + ice::Span _conditions; + ice::Span _steps; + ice::Span _modifiers; + ice::Span _constants; + ice::Span _constant_values; + ice::String _strings; + }; + + auto create_input_action_layer( + ice::Allocator& alloc, + ice::Data layer_data + ) noexcept -> ice::UniquePtr + { + ice::Memory const data_copy = alloc.allocate({ layer_data.size, layer_data.alignment }); + ice::memcpy(data_copy, layer_data); + + ice::Expected params = ice::load_from_data(ice::data_view(data_copy)); + if (params.succeeded() == false) + { + return {}; + } + + return ice::make_unique(alloc, alloc, data_copy, params.value()); + } + + auto create_input_action_layer( + ice::Allocator& alloc, + ice::Memory layer_data + ) noexcept -> ice::UniquePtr + { + ice::Expected params = ice::load_from_data(ice::data_view(layer_data)); + if (params.succeeded() == false) + { + return {}; + } + + return ice::make_unique(alloc, alloc, layer_data, params.value()); + } + + auto parse_input_action_layer( + ice::Allocator& alloc, + ice::String definition + ) noexcept -> ice::Array> + { + struct MultiLayerScriptParser : InputActionScriptParser + { + ice::Array> results; + + MultiLayerScriptParser(ice::Allocator& alloc) noexcept + : InputActionScriptParser{ alloc } + , results{ alloc } + { + } + + void on_layer_parsed(ice::UniquePtr layer) noexcept override + { + ice::array::push_back(results, ice::move(layer)); + } + } parser{ alloc }; + + bool const parse_success = ice::asl::parse_action_input_definition(alloc, definition, parser); + ICE_LOG_IF(parse_success == false, LogSeverity::Warning, LogTag::Engine, "Failed to parse input action layer script!"); + return ice::move(parser.results); + } + + auto save_input_action_layer( + ice::Allocator& alloc, + ice::InputActionLayer const& action_layer + ) noexcept -> ice::Expected + { + return {}; + } + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_layer_builder.cxx b/source/code/systems/input_action_system/private/input_action_layer_builder.cxx new file mode 100644 index 00000000..f7c7aa27 --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_layer_builder.cxx @@ -0,0 +1,697 @@ +#include "input_action_internal_types.hxx" +#include +#include +#include +#include +#include + +namespace ice +{ + + namespace detail + { + + auto parse_source(ice::String source) noexcept -> std::tuple + { + ice::u8 read_from = 0; + ice::ucount source_size = ice::size(source); + // We want to parse the following cases: (.[xyz]) + if (source_size > 1 && source[source_size - 2] == '.') + { + source_size -= 2; + read_from = source[source_size + 1] - 'x'; + ICE_ASSERT_CORE(read_from >= 0 && read_from < 3); + } + return { ice::string::substr(source, 0, source_size), read_from }; + } + + } // namespace detail + + class SimpleInputActionLayerBuilder; + + using ActionBuilderModifier = InputActionModifierData; + + struct ActionBuilderStep + { + ice::InputActionStep step; + ice::HeapString<> source; + + //! \brief Axis to 'source[axis_to_read]' and 'target[axis_to_write]'. [to_read, to_write] + ice::arr<2, ice::u8> axis; + + //! \brief Contains info the the step source should be taken from an action instead. + bool from_action = false; + }; + + struct ActionBuilderCondition + { + ActionBuilderCondition( + ice::Allocator& alloc, + ice::String name, + ice::InputActionCondition condition, + ice::InputActionConditionFlags flags, + ice::f32 param, + ice::u8 axis, + bool from_action + ) noexcept + : condition{ condition } + , flags{ flags } + , source{ alloc, name } + , steps{ alloc } + , param{ param } + , axis{ axis } + , from_action{ from_action } + { + } + + //! \brief The check to be performed. + ice::InputActionCondition condition; + + //! \brief Flags that may affect how conditions are executed. + ice::InputActionConditionFlags flags; + + //! \brief The name of the source which values to use. + ice::HeapString<> source; + + //! \brief Steps to be executed. + ice::Array steps; + + ice::f32 param; + + ice::u8 axis; + + //! \brief Contains info the the condition source should be taken from an action instead. + bool from_action = false; + }; + + template<> + struct InputActionBuilder::BuilderBase::Internal + { + + }; + + template<> + struct InputActionBuilder::BuilderBase::Internal : InputActionBuilder::BuilderBase::Internal + { + void destroy(ice::Allocator& alloc) noexcept + { + alloc.destroy(this); + } + }; + + template<> + struct InputActionBuilder::BuilderBase::Internal + { + Internal(ice::Allocator& alloc, ice::String name, ice::InputActionSourceType type) noexcept + : name{ alloc, name } + , type{ type } + , events{ alloc } + { + ice::hashmap::reserve(events, 2); + } + + ice::HeapString<> name; + ice::InputActionSourceType type; + ice::HashMap events; + }; + + template<> + struct InputActionBuilder::BuilderBase::Internal + { + Internal(ice::Allocator& alloc) noexcept + : allocator{ alloc } + , conditions{ alloc } + { + ice::array::reserve(conditions, 4); + } + + void add_step(ActionBuilderStep&& step) noexcept + { + ice::array::push_back( + ice::array::back(conditions).steps, + ice::move(step) + ); + } + + void add_condition(ActionBuilderCondition&& condition) noexcept + { + ice::array::push_back(conditions, ice::move(condition)); + } + + void finalize() noexcept + { + ActionBuilderCondition& final_condition = ice::array::back(conditions); + + // Ensure this series is finished after this condition. + final_condition.flags |= InputActionConditionFlags::SeriesFinish; + } + + public: + ice::Allocator& allocator; + ice::Array conditions; + }; + + template<> + struct InputActionBuilder::BuilderBase::Internal + { + Internal( + ice::Allocator& alloc, + ice::String name, + ice::InputActionDataType type + ) noexcept + : allocator{ alloc } + , name{ alloc, name } + , type{ type } + , cond_series{ alloc } + , modifiers{ alloc } + { + ice::array::reserve(cond_series, 3); + ice::array::reserve(modifiers, 2); + } + + ice::Allocator& allocator; + ice::HeapString<> name; + ice::InputActionDataType type; + ice::InputActionBehavior behavior; + ice::Array> cond_series; + ice::Array modifiers; + + auto add_condition_series() noexcept -> InputActionBuilder::ConditionSeries + { + ice::array::push_back(cond_series, Internal{ allocator }); + Internal* const series_ptr = ice::addressof(ice::array::back(cond_series)); + return { series_ptr }; + } + + void finalize() + { + for (Internal& series : this->cond_series) + { + series.finalize(); + } + } + }; + + + class SimpleInputActionLayerBuilder : public ice::InputActionBuilder::Layer + { + public: + SimpleInputActionLayerBuilder( + ice::Allocator& alloc, + ice::String name + ) noexcept + : Layer{ alloc.create>() } + , _allocator{ alloc } + , _constants{ _allocator } + , _sources{ _allocator } + , _actions{ _allocator } + , _name{ _allocator, name } + { + ice::hashmap::reserve(_sources, 16); + ice::hashmap::reserve(_actions, 10); + ice::array::push_back(_constants, { InputActionConstant::Nil, 0.0f }); + } + + ~SimpleInputActionLayerBuilder() + { + this->internal().destroy(_allocator); + } + + auto set_name( + ice::String name + ) noexcept -> ice::InputActionBuilder::Layer& override + { + _name = name; + return *this; + } + + void set_constant( + ice::InputActionConstant constant, + ice::f32 value + ) noexcept override + { + ice::array::push_back(_constants, { constant, value }); + } + + auto define_source( + ice::String name, + ice::InputActionSourceType type + ) noexcept -> ice::InputActionBuilder::Source override + { + ice::hashmap::set(_sources, ice::hash(name), {_allocator, name, type}); + return { ice::hashmap::try_get(_sources, ice::hash(name)) }; + } + + auto define_action( + ice::String name, + ice::InputActionDataType type + ) noexcept -> ice::InputActionBuilder::Action override + { + ice::hashmap::set(_actions, ice::hash(name), {_allocator, name, type}); + return { ice::hashmap::try_get(_actions, ice::hash(name)) }; + } + + auto finalize(ice::Allocator& alloc) noexcept -> ice::UniquePtr override + { + ice::u16 count_storage_values = 0; + + ice::HeapString<> strings{ _allocator }; + ice::Array final_sources{ _allocator }; + ice::Array final_steps{ _allocator }; + ice::Array final_conditions{ _allocator }; + ice::Array final_modifiers{ _allocator }; + ice::Array final_actions{ _allocator }; + + // Insert layer name as the first string + ice::string::push_back(strings, _name); + ice::string::push_back(strings, '\0'); + + // Prepare data of all sources + for (Internal const& source : _sources) + { + if (ice::hashmap::empty(source.events)) + { + ice::array::push_back(final_sources, + InputActionSourceInputInfo{ + .name = { ice::u16(ice::size(strings)), ice::u16(ice::size(source.name)) }, + .input = ice::input::InputID::Invalid, + .type = source.type, + .storage_offset = count_storage_values, + .param = 0.0f, + } + ); + } + + for (ice::input::InputID input_event : source.events) + { + ice::array::push_back(final_sources, + InputActionSourceInputInfo{ + .name = { ice::u16(ice::size(strings)), ice::u16(ice::size(source.name)) }, + .input = input_event, + .type = source.type, + .storage_offset = count_storage_values, + .param = 0.0f, + } + ); + } + ice::string::push_back(strings, source.name); + // ice::string::push_back(strings, '\0'); + + switch(source.type) + { + using enum InputActionSourceType; + case Key: + case Button: + case Trigger: + case Axis1d: count_storage_values += 1; break; + case Axis2d: count_storage_values += 2; break; + default: ICE_ASSERT_CORE(false); break; + } + } + + auto find_source_storage_index = [&strings, &final_sources](ice::String source_name) noexcept -> ice::u16 + { + ice::ucount idx_found = ice::ucount_max; + bool const found = ice::search( + ice::array::slice(final_sources), + source_name, + [&strings](ice::InputActionSourceInputInfo const& source, ice::String expected) noexcept + { + ice::String const source_name = ice::string::substr( + strings, source.name.offset, source.name.size + ); + return expected == source_name; + }, + idx_found + ); + + ICE_ASSERT_CORE(found && idx_found < ice::array::count(final_sources)); + ice::u16 const source_storage_idx = final_sources[idx_found].storage_offset; + return source_storage_idx; + }; + + auto find_action_storage_index = [&strings, &final_actions](ice::String source_name) noexcept -> ice::u16 + { + ice::ucount idx_found = ice::ucount_max; + bool const found = ice::search( + ice::array::slice(final_actions), + source_name, + [&strings](ice::InputActionInfo const& action, ice::String expected) noexcept + { + ice::String const source_name = ice::string::substr( + strings, action.name.offset, action.name.size + ); + return expected == source_name; + }, + idx_found + ); + + ICE_ASSERT_CORE(found && idx_found < ice::array::count(final_actions)); + return ice::u16(idx_found); + }; + + // Run finalization on all internal action objects. + for (Internal& action : ice::hashmap::values(_actions)) + { + action.finalize(); + } + + // Prepare data of all actions + ice::u16 step_offset = 0, step_count = 0; + ice::u16 condition_offset = 0, condition_count = 0; + ice::u8 modifier_offset = 0, modifier_count = 0; + + for (Internal const& action : _actions) + { + for (Internal const& series : action.cond_series) + { + for (ActionBuilderCondition const& condition : series.conditions) + { + for (ActionBuilderStep const& step : condition.steps) + { + if (step.step < InputActionStep::Set) + { + ice::array::push_back(final_steps, + InputActionStepData{ + .source = { 0, 0 }, + .id = step.step, + .dst_axis = 0 + } + ); + } + else + { + ice::array::push_back(final_steps, + InputActionStepData{ + .source = { + .source_index = find_source_storage_index(step.source), + .source_axis = step.axis.x + }, + .id = step.step, + .dst_axis = step.axis.y + } + ); + } + + step_count += 1; + } + + ICE_ASSERT_CORE(action.type != InputActionDataType::Invalid); + ice::InputActionIndex source_index = { .source_index = 0,.source_axis = 0 }; + if (condition.from_action) + { + // If we are empty, it's a "self reference" + if (ice::string::any(condition.source)) + { + source_index.source_index = find_action_storage_index(condition.source); + source_index.source_axis = condition.axis; + } + else + { + source_index.source_index = InputActionIndex::SelfIndex; + } + } + else /*if (condition.condition != InputActionCondition::AlwaysTrue + && condition.condition != InputActionCondition::ActionActive + && condition.condition != InputActionCondition::ActionInactive)*/ + { + source_index.source_index = find_source_storage_index(condition.source); + source_index.source_axis = condition.axis; + } + + ice::array::push_back(final_conditions, + InputActionConditionData{ + .source = source_index, + .id = condition.condition, + .flags = condition.flags, + .steps = { step_offset, step_count }, + .param = condition.param + } + ); + + step_offset += ice::exchange(step_count, ice::u16_0); + condition_count += 1; + } + } // for (ConditionSeries& series : ...) + + modifier_count = ice::u8(ice::count(action.modifiers)); + ice::array::push_back(final_modifiers, action.modifiers); + + ice::array::push_back(final_actions, + InputActionInfo{ + .name = { ice::u16(ice::size(strings)), ice::u16(ice::size(action.name)) }, + .type = action.type, + .behavior = action.behavior, + .conditions = { condition_offset, condition_count }, + .mods = { modifier_offset, modifier_count } + } + ); + + ice::string::push_back(strings, action.name); + + modifier_offset += modifier_count; + condition_offset += ice::exchange(condition_count, ice::u16_0); + } + + ice::Array final_constant_values{ alloc }; + ice::Array final_constants{ alloc }; + for (auto [constant, value] : _constants) + { + ice::u8 const offset = (ice::u8) ice::count(final_constant_values); + ice::array::push_back(final_constants, { .identifier = constant, .offset = offset }); + ice::array::push_back(final_constant_values, value); + } + + ice::InputActionLayerInfoHeader final_info{ + .size_name = ice::u8(ice::size(_name)), + .count_constants = ice::u8(ice::count(final_constants)), + .count_sources = ice::u16(ice::count(final_sources)), + .count_actions = ice::u16(ice::count(final_actions)), + .count_conditions = ice::u16(ice::count(final_conditions)), + .count_steps = ice::u16(ice::count(final_steps)), + .count_modifiers = ice::u16(ice::count(final_modifiers)), + .offset_strings = 0, + }; + + // Allocate memory and copy all data + ice::meminfo minfo_layer = ice::meminfo_of; + ice::usize const offset_sources = minfo_layer += ice::array::meminfo(final_sources); + ice::usize const offset_actions = minfo_layer += ice::array::meminfo(final_actions); + ice::usize const offset_conditions = minfo_layer += ice::array::meminfo(final_conditions); + ice::usize const offset_steps = minfo_layer += ice::array::meminfo(final_steps); + ice::usize const offset_modifiers = minfo_layer += ice::array::meminfo(final_modifiers); + ice::usize const offset_constant_values = minfo_layer += ice::meminfo_of * ice::count(final_constant_values); + ice::usize const offset_constants = minfo_layer += ice::array::meminfo(final_constants); + ice::usize const offset_strings = minfo_layer += ice::string::meminfo(ice::String{strings}); + final_info.offset_strings = ice::u32(offset_strings.value); + + ice::Memory const final_memory = alloc.allocate(minfo_layer); + ice::memcpy(final_memory, ice::data_view(final_info)); + ice::memcpy(ice::ptr_add(final_memory, offset_sources), ice::array::data_view(final_sources)); + ice::memcpy(ice::ptr_add(final_memory, offset_actions), ice::array::data_view(final_actions)); + ice::memcpy(ice::ptr_add(final_memory, offset_conditions), ice::array::data_view(final_conditions)); + ice::memcpy(ice::ptr_add(final_memory, offset_steps), ice::array::data_view(final_steps)); + ice::memcpy(ice::ptr_add(final_memory, offset_modifiers), ice::array::data_view(final_modifiers)); + ice::memcpy(ice::ptr_add(final_memory, offset_constant_values), ice::array::data_view(final_constant_values)); + ice::memcpy(ice::ptr_add(final_memory, offset_constants), ice::array::data_view(final_constants)); + ice::memcpy(ice::ptr_add(final_memory, offset_strings), ice::string::data_view(strings)); + return ice::create_input_action_layer(alloc, final_memory); + } + + private: + ice::Allocator& _allocator; + ice::Array> _constants; + ice::HashMap> _sources; + ice::HashMap> _actions; + ice::HeapString<> _name; + }; + + auto InputActionBuilder::Source::add_key(ice::input::KeyboardKey key) noexcept -> Source& + { + using ice::input::DeviceType; + using enum ice::InputActionSourceType; + + ICE_ASSERT_CORE(internal().type == Key || internal().type == Button); + ice::hashmap::set(internal().events, ice::hash(key), input_identifier(DeviceType::Keyboard, key)); + return *this; + } + + auto InputActionBuilder::Source::add_keymod(ice::input::KeyboardMod keymod) noexcept -> Source& + { + using ice::input::DeviceType; + using enum ice::InputActionSourceType; + + ice::input::InputID const iid = input_identifier(DeviceType::Keyboard, keymod, ice::input::mod_identifier_base_value); + + ICE_ASSERT_CORE(internal().type == Key || internal().type == Button); + ice::hashmap::set(internal().events, ice::hash(iid), iid); + return *this; + } + + auto InputActionBuilder::Source::add_button(ice::input::MouseInput button) noexcept -> Source& + { + using ice::input::DeviceType; + using enum ice::InputActionSourceType; + + ICE_ASSERT_CORE(internal().type == Key || internal().type == Button); + ice::hashmap::set(internal().events, ice::hash(button), input_identifier(DeviceType::Mouse, button)); + return *this; + } + + auto InputActionBuilder::Source::add_button(ice::input::ControllerInput button) noexcept -> Source& + { + using ice::input::DeviceType; + using enum ice::InputActionSourceType; + + ICE_ASSERT_CORE(internal().type == Key || internal().type == Button); + ice::hashmap::set(internal().events, ice::hash(button), input_identifier(DeviceType::Controller, button)); + return *this; + } + + auto InputActionBuilder::Source::add_axis(ice::input::MouseInput axis) noexcept -> Source& + { + using ice::input::DeviceType; + using ice::input::MouseInput; + using enum ice::InputActionSourceType; + + ICE_ASSERT_CORE(internal().type == Axis2d); + ICE_ASSERT_CORE(axis == MouseInput::PositionX); + if (axis == MouseInput::PositionX) + { + ice::hashmap::set(internal().events, ice::hash(MouseInput::PositionX), input_identifier(DeviceType::Mouse, MouseInput::PositionX)); + ice::hashmap::set(internal().events, ice::hash(MouseInput::PositionY), input_identifier(DeviceType::Mouse, MouseInput::PositionY)); + } + return *this; + } + + auto InputActionBuilder::Source::add_axis(ice::input::ControllerInput axis) noexcept -> Source& + { + using ice::input::DeviceType; + using ice::input::ControllerInput; + using enum ice::InputActionSourceType; + + ICE_ASSERT_CORE(internal().type == Axis2d); + ICE_ASSERT_CORE(axis == ControllerInput::LeftAxisX || axis == ControllerInput::RightAxisX); + if (axis == ControllerInput::LeftAxisX) + { + ice::hashmap::set(internal().events, ice::hash(axis), input_identifier(DeviceType::Controller, ControllerInput::LeftAxisX)); + ice::hashmap::set(internal().events, ice::hash(axis), input_identifier(DeviceType::Controller, ControllerInput::LeftAxisY)); + } + else if (axis == ControllerInput::RightAxisX) + { + ice::hashmap::set(internal().events, ice::hash(axis), input_identifier(DeviceType::Controller, ControllerInput::RightAxisX)); + ice::hashmap::set(internal().events, ice::hash(axis), input_identifier(DeviceType::Controller, ControllerInput::RightAxisY)); + } + return *this; + } + + void InputActionBuilder::ConditionSeries::set_finished(bool can_finalize_condition_checks) noexcept + { + if (can_finalize_condition_checks) + { + ice::array::back(internal().conditions).flags |= InputActionConditionFlags::Final; + } + else + { + ice::array::back(internal().conditions).flags |= InputActionConditionFlags::SeriesFinish; + } + } + + auto InputActionBuilder::ConditionSeries::add_condition( + ice::String source, + ice::InputActionCondition condition, + ice::InputActionConditionFlags flags /*= None*/, + ice::f32 param /*= 0.0f*/, + bool from_action /*= false*/ + ) noexcept -> ConditionSeries& + { + auto const[source_name, read_from] = detail::parse_source(source); + + internal().add_condition( + ActionBuilderCondition{ + internal().allocator, + source_name, + condition, + flags, + param, + read_from, + from_action + } + ); + + return *this; + } + + auto InputActionBuilder::ConditionSeries::add_step( + ice::InputActionStep step + ) noexcept -> ConditionSeries& + { + internal().add_step({ + .step = step, + .source = ice::HeapString<>{ internal().allocator }, + .axis = { 0, 0 } + }); + return *this; + } + + auto InputActionBuilder::ConditionSeries::add_step( + ice::String source, + ice::InputActionStep step, + ice::String target_axis /*= ".x"*/ + ) noexcept -> ConditionSeries& + { + auto const [source_name, read_from] = detail::parse_source(source); + auto const [_, write_to] = detail::parse_source(target_axis); + + internal().add_step({ + .step = step, + .source = { internal().allocator, source_name }, + .axis = { read_from, write_to } + }); + return *this; + } + + auto InputActionBuilder::Action::add_condition_series() noexcept -> ConditionSeries + { + return internal().add_condition_series(); + } + + auto InputActionBuilder::Action::set_behavior( + ice::InputActionBehavior behavior + ) noexcept -> Action& + { + internal().behavior = behavior; + return *this; + } + + auto InputActionBuilder::Action::add_modifier( + ice::InputActionModifier modifier, + ice::f32 param, + ice::String target_axis /*= ".x"*/ + ) noexcept -> Action& + { + ICE_ASSERT_CORE(ice::size(target_axis) >= 2 && target_axis[0] == '.'); + if (ice::size(target_axis) < 2 || target_axis[0] != '.') + { + return *this; + } + + for (char axis_component : ice::string::substr(target_axis, 1, 3)) + { + ICE_ASSERT_CORE(axis_component >= 'x' && axis_component <= 'z'); // .xyz + + ice::u8 const axis = axis_component - 'x'; + ice::array::push_back(internal().modifiers, { .id = modifier, .axis = axis, .param = param }); + } + return *this; + } + + auto create_input_action_layer_builder( + ice::Allocator& alloc, + ice::String name + ) noexcept -> ice::UniquePtr + { + return ice::make_unique(alloc, alloc, name); + } + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_script.cxx b/source/code/systems/input_action_system/private/input_action_script.cxx new file mode 100644 index 00000000..0f1f93e5 --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script.cxx @@ -0,0 +1,108 @@ +#include "input_action_script.hxx" +#include "input_action_script_grammar.hxx" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace ice::asl +{ + + struct ASLNodeAllocator final : public arctic::SyntaxNodeAllocator + { + ASLNodeAllocator(ice::Allocator& backing) noexcept + : _backing{ backing } + { } + + auto allocate(arctic::usize size, arctic::usize align) noexcept -> void* override + { + ice::AllocResult const result = _backing.allocate({ ice::usize{ size }, ice::ualign(align) }); + return result.memory; + } + + void deallocate(void* ptr) noexcept override + { + return _backing.deallocate(ptr); + } + + private: + ice::Allocator& _backing; + }; + + struct ASLEvents : arctic::SyntaxVisitorGroup + { + using arctic::SyntaxVisitor::visit; + + void visit(arctic::SyntaxNode node) noexcept + { + root = node; + } + + arctic::SyntaxNode root; + }; + + //! \brief A default implementation of a tokenizer for Arctic language. + //! + //! \param word [inout] A word to be tokenized. Update the value with the next word that was not tokenized. + //! \param processor [in] A statefull generator object used to access the next word value. + //! \param location [in] A pre-calculated source location to be assigned to the resulting token. + //! Provides properly calculated line and column values according to options, newlines and whitespace characers. + auto asil_lexer_tokenizer( + arctic::Word& word, + arctic::WordProcessor& processor, + arctic::TokenLocation location + ) noexcept -> arctic::Token + { + arctic::Token result{ + .value = word.value, + .type = arctic::TokenType::Invalid, + .location = location + }; + + ice::ucount idx; + ice::asl::TokenDefinition const needle{ .value = word.value }; + if (ice::binary_search(ice::span::from_std_const(Constant_TokenDefinitions), needle, idx)) + { + result.type = Constant_TokenDefinitions[idx].type; + word = processor.next(); + } + else + { + result = arctic::arctic_lexer_tokenizer(word, processor, location); + } + return result; + } + + bool parse_action_input_definition( + ice::Allocator& allocator, + ice::String definition, + ice::asl::ActionInputParserEvents& handler + ) noexcept + { + ice::asl::ASLNodeAllocator node_alloc{ allocator }; + + arctic::WordMatcher matcher{}; + arctic::initialize_default_matcher(&matcher); + + arctic::Lexer lexer = arctic::create_lexer( + arctic::create_word_processor(definition, &matcher), + {.tokenizer = asil_lexer_tokenizer} + ); + + std::unique_ptr parser = arctic::create_default_parser( + { .rules = ice::asl::grammar::ScriptRules } + ); + + arctic::SyntaxVisitor* visitors[]{ &handler }; + bool const result = parser->parse(lexer, node_alloc, visitors); + arctic::shutdown_matcher(&matcher); + return result; + } + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_script.hxx b/source/code/systems/input_action_system/private/input_action_script.hxx new file mode 100644 index 00000000..3df1d813 --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script.hxx @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include +#include +#include +#include "input_action_script_syntax_data.hxx" + +#include + +namespace ice::asl +{ + + struct ActionInputParserEvents : arctic::SyntaxVisitorGroup + { + void visit(arctic::SyntaxNode<> node) noexcept override final + { + SyntaxVisitorGroup::visit(node); + } + + void visit(arctic::SyntaxNode node) noexcept override = 0; + }; + + bool parse_action_input_definition( + ice::Allocator& allocator, + ice::String definition, + ice::asl::ActionInputParserEvents& handler + ) noexcept; + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_script_grammar.cxx b/source/code/systems/input_action_system/private/input_action_script_grammar.cxx new file mode 100644 index 00000000..6893d95d --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script_grammar.cxx @@ -0,0 +1,92 @@ +#include "input_action_script_grammar.hxx" + +namespace ice::asl::grammar +{ + + auto Condition::internal_condition_matcher( + arctic::SyntaxRule const&, + arctic::MatchContext& ctx + ) noexcept -> arctic::ParseState + { + using arctic::MatchContext; + using arctic::ParseState; + using arctic::SyntaxNode; + using arctic::Token; + + static constexpr SyntaxRule cond_when{ WhenCondition, MatchAll }; + static constexpr SyntaxRule cond_orand{ AndOrCondition, MatchAll }; + static constexpr SyntaxRule cond_flags{ ConditionFlags, MatchAll }; + static constexpr SyntaxRule steps{ ActionStep::Rules, MatchAll }; + + ParseState result = ParseState::Success; + SyntaxNode parent = ctx.node; + SyntaxNode condition{ }; + MatchContext local_ctx = ctx; + + if (ctx.token.type != TokenType::ASL_KW_When) + { + return ParseState::Error_UnexpectedToken; + } + // We require 'when' conditions to be follwing a newline token. + if (ctx.prev_token.type != TokenType::ST_EndOfLine) + { + return ParseState::Error_UnexpectedToken; + } + + // Add a new child node and store the condition type. + local_ctx.node = condition = parent.append_child(SyntaxNode{ local_ctx.alloc }); + result = cond_when.execute(local_ctx); + + do + { + Token tok = local_ctx.token; + if (tok.type == TokenType::ASL_KW_WhenOr + || tok.type == TokenType::ASL_KW_WhenAnd) + { + // Add a new sibling node and store the condition type. + local_ctx.node = condition = condition.append_sibling(SyntaxNode{ local_ctx.alloc }); + result = cond_orand.execute(local_ctx); + } + else if (tok.type == TokenType::CT_Dot) + { + MatchContext stepsctx = local_ctx; + while (result == ParseState::Success && tok.type == TokenType::CT_Dot) + { + stepsctx.node = condition.append_child(SyntaxNode{ local_ctx.alloc }); + result = steps.execute(stepsctx); + tok = stepsctx.token; + } + + local_ctx.token = stepsctx.token; + local_ctx.prev_token = stepsctx.prev_token; + } + else if (tok.type == TokenType::CT_Comma) + { + result = cond_flags.execute(local_ctx); + } + else if (tok.type == TokenType::ST_EndOfLine) + { + local_ctx.prev_token = ice::exchange(local_ctx.token, local_ctx.lexer.next()); + switch (local_ctx.token.type) + { + case TokenType::ASL_KW_WhenOr: // For additional conditions + case TokenType::ASL_KW_WhenAnd: + case TokenType::CT_Comma: + case TokenType::CT_Dot: break; // For steps + case TokenType::ASL_KW_When: + case TokenType::ASL_KW_Modifier: + default: result = ParseState::Error_UnexpectedToken; break; + } + } + else + { + break; + } + } while (result == ParseState::Success); + + ctx.token = local_ctx.token; + ctx.prev_token = local_ctx.prev_token; + return result; + } + +} // namespace ice::asl::grammar diff --git a/source/code/systems/input_action_system/private/input_action_script_grammar.hxx b/source/code/systems/input_action_system/private/input_action_script_grammar.hxx new file mode 100644 index 00000000..df27f699 --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script_grammar.hxx @@ -0,0 +1,341 @@ +#pragma once +#include "input_action_script_syntax_data.hxx" +#include "input_action_script_tokens.hxx" +#include +#include + +namespace ice::asl::grammar +{ + using arctic::MatchAll; + using arctic::MatchFirst; + using arctic::MatchChild; + using arctic::MatchSibling; + using arctic::SyntaxRule; + + struct TokenList + { + + static constexpr SyntaxRule NumberTypes[]{ + SyntaxRule{ TokenType::CT_Number }, + SyntaxRule{ TokenType::CT_NumberFloat }, + }; + + static constexpr SyntaxRule DeviceKeywords[]{ + SyntaxRule{ TokenType::ASL_KW_Controller }, + SyntaxRule{ TokenType::ASL_KW_Keyboard }, + SyntaxRule{ TokenType::ASL_KW_Mouse }, + }; + + static constexpr SyntaxRule InputNativeTypes[]{ // button, axis1d, axis2d or axis3d + SyntaxRule{ TokenType::ASL_NT_Button }, + SyntaxRule{ TokenType::ASL_NT_Axis1D }, + SyntaxRule{ TokenType::ASL_NT_Axis2D }, + SyntaxRule{ TokenType::ASL_NT_Axis3D }, + }; + + static constexpr SyntaxRule SymbolCategories[]{ + SyntaxRule{ TokenType::ASL_KW_Source }, + SyntaxRule{ TokenType::ASL_KW_Action }, + }; + + static constexpr SyntaxRule StepCommands[]{ + SyntaxRule{ TokenType::ASL_OP_Activate }, + SyntaxRule{ TokenType::ASL_OP_Deactivate }, + SyntaxRule{ TokenType::ASL_OP_Toggle }, + SyntaxRule{ TokenType::ASL_OP_Reset }, + SyntaxRule{ TokenType::ASL_OP_Time }, + }; + + static constexpr SyntaxRule ActionTypes[]{ // button, axis1d, axis2d or axis3d + SyntaxRule{ TokenType::ASL_NT_Bool }, + SyntaxRule{ TokenType::ASL_NT_Float1 }, + SyntaxRule{ TokenType::ASL_NT_Float2 }, + SyntaxRule{ TokenType::ASL_NT_Float3 }, + SyntaxRule{ TokenType::ASL_NT_Object }, + }; + + static constexpr SyntaxRule StepValueOperations[]{ + SyntaxRule{ TokenType::OP_Plus }, + SyntaxRule{ TokenType::OP_Minus }, + SyntaxRule{ TokenType::OP_Assign }, + }; + + template + static auto RuleFn_IsCharacter(arctic::SyntaxRule const&, arctic::MatchContext& ctx) noexcept -> arctic::ParseState + { + using enum arctic::ParseState; + return ctx.token.value[0] == ComponentChar ? Success : Error_UnexpectedToken; + } + + static constexpr SyntaxRule XYZ[]{ + SyntaxRule{ RuleFn_IsCharacter<'x'> }, + SyntaxRule{ RuleFn_IsCharacter<'y'> }, + SyntaxRule{ RuleFn_IsCharacter<'z'> }, + }; + }; + + template + struct Symbol + { + template + struct StoreFnSelector + { + static constexpr auto StoreFn = SyntaxRule::store_value; + }; + + template + struct StoreFnSelector + { + static constexpr auto StoreFn = NameField == CategoryField + ? SyntaxRule::store_value_extend + : SyntaxRule::store_value; + }; + + static constexpr auto StoreFn = StoreFnSelector + ::StoreFn; + + + static constexpr SyntaxRule Category[]{ + SyntaxRule{ TokenList::SymbolCategories, MatchFirst, CategoryField }, + SyntaxRule{ TokenType::CT_Dot }, + }; + + static constexpr SyntaxRule AxisComponent[]{ + SyntaxRule{ TokenType::CT_Dot }, + SyntaxRule{ TokenList::XYZ, MatchFirst, NameField, SyntaxRule::store_value_extend } + }; + + static constexpr SyntaxRule NameExtended[]{ + SyntaxRule{ TokenType::CT_Symbol, NameField, SyntaxRule::store_value_extend }, + }; + + static constexpr SyntaxRule NameWithCategory[]{ + SyntaxRule{ Category, MatchAll }.optional(), + SyntaxRule{ TokenType::CT_Symbol, NameField, StoreFn }, + }; + + static constexpr SyntaxRule NameWithComponents[]{ + SyntaxRule{ TokenType::CT_Symbol, NameField }, // Capture name + SyntaxRule{ AxisComponent, MatchAll }.optional() // ... and components + }; + + static constexpr SyntaxRule FullName[]{ + SyntaxRule{ Category, MatchAll }.optional(), + SyntaxRule{ TokenType::CT_Symbol, NameField, StoreFn }, + SyntaxRule{ AxisComponent, MatchAll }.optional() + }; + }; + + struct Constant + { + using SymbolType = Symbol<&LayerConstant::name>; + + static constexpr SyntaxRule Rules[]{ + SyntaxRule{ TokenType::ASL_KW_Constant }, + SyntaxRule{ TokenType::CT_Symbol, &LayerConstant::name }, + SyntaxRule{ TokenType::CT_Dot }, + SyntaxRule{ SymbolType::NameExtended, MatchAll }, + SyntaxRule{ TokenType::OP_Assign }, + SyntaxRule{ TokenList::NumberTypes, MatchFirst, &LayerConstant::param }, + SyntaxRule{ TokenType::ST_EndOfLine }, + }; + }; + + struct SourceBinding + { + static constexpr SyntaxRule SourceBindingInfo[]{ + SyntaxRule{ TokenList::DeviceKeywords, MatchFirst, &LayerSourceBinding::device }, + SyntaxRule{ TokenType::CT_Dot }, + SyntaxRule{ TokenType::CT_Symbol, &LayerSourceBinding::source }, + }; + + static constexpr SyntaxRule SourceBindingNext[]{ + SyntaxRule{ TokenType::CT_Comma }, + SyntaxRule{ SourceBindingInfo, MatchAll } + }; + + static constexpr SyntaxRule Rules[]{ + SyntaxRule{ TokenType::CT_Colon }, + SyntaxRule{ SourceBindingInfo, MatchAll }, + SyntaxRule{ SourceBindingNext, MatchSibling }.optional().repeat(), + }; + }; + + struct ActionStep + { + using SymbolType = Symbol<&LayerActionStep::source, &LayerActionStep::source_type>; + + static constexpr SyntaxRule CommandOperation[]{ + SyntaxRule{ TokenList::StepCommands, MatchFirst, &LayerActionStep::step } + }; + + static constexpr SyntaxRule ArithmeticOperation[]{ + SyntaxRule{ TokenList::XYZ, MatchFirst, &LayerActionStep::destination, SyntaxRule::store_value_extend }, + SyntaxRule{ TokenList::StepValueOperations, MatchFirst, &LayerActionStep::step }, + SyntaxRule{ SymbolType::FullName, MatchAll } + }; + + static constexpr SyntaxRule OperationList[]{ + SyntaxRule{ CommandOperation, MatchAll }, + SyntaxRule{ ArithmeticOperation, MatchAll }, + }; + + static constexpr SyntaxRule Rules[]{ + // If we don't end up using the destination string it's fine + // but we require the '.' character to be used when setting destinations via builder. + SyntaxRule{ TokenType::CT_Dot, &LayerActionStep::destination }, + SyntaxRule{ OperationList, MatchFirst }, + SyntaxRule{ TokenType::ST_EndOfLine }, // We need to start and end with newlines + }; + }; + + struct Condition + { + using SymbolType = Symbol<&LayerActionCondition::source_name, &LayerActionCondition::source_type>; + + static constexpr SyntaxRule StateCheck[]{ + SyntaxRule{ TokenType::ASL_OP_IsPressed }, + SyntaxRule{ TokenType::ASL_OP_IsReleased }, + SyntaxRule{ TokenType::ASL_OP_IsActive }, + SyntaxRule{ TokenType::ASL_OP_IsInactive }, + SyntaxRule{ TokenType::KW_True }, + }; + + static constexpr SyntaxRule ValueConditions[]{ + SyntaxRule{ TokenType::OP_Equal }, + SyntaxRule{ TokenType::OP_NotEqual }, + SyntaxRule{ TokenType::OP_Less }, + SyntaxRule{ TokenType::OP_LessOrEqual }, + SyntaxRule{ TokenType::OP_Greater }, + SyntaxRule{ TokenType::OP_GreaterOrEqual }, + }; + + static constexpr SyntaxRule ValueCheck[]{ + SyntaxRule{ TokenList::XYZ, MatchFirst, &LayerActionCondition::source_name, arctic::SyntaxRule::store_value_extend }, + SyntaxRule{ ValueConditions, MatchFirst, &LayerActionCondition::condition }, + SyntaxRule{ TokenList::NumberTypes, MatchFirst, &LayerActionCondition::param } + }; + + static constexpr SyntaxRule ConditionChecks[]{ + SyntaxRule{ StateCheck, MatchFirst, &LayerActionCondition::condition }, + SyntaxRule{ ValueCheck, MatchAll }, + }; + + static constexpr SyntaxRule ConditionExpression[]{ + SyntaxRule{ TokenType::CT_Dot }, + SyntaxRule{ ConditionChecks, MatchFirst }, + }; + + static constexpr SyntaxRule FlagList[]{ + SyntaxRule{ TokenType::ASL_KWF_CheckSeries, &LayerActionCondition::flag_series } + }; + + static constexpr SyntaxRule ConditionFlags[]{ + SyntaxRule{ TokenType::CT_Comma }, + SyntaxRule{ FlagList, MatchFirst }, + SyntaxRule{ TokenType::ST_EndOfLine }.noadvance(), + }; + + static constexpr SyntaxRule AndOrTokens[]{ + SyntaxRule{ TokenType::ASL_KW_WhenAnd }, + SyntaxRule{ TokenType::ASL_KW_WhenOr }, + }; + + static constexpr SyntaxRule WhenCondition[]{ + SyntaxRule{ TokenType::ASL_KW_When, &LayerActionCondition::type }, + SyntaxRule{ SymbolType::NameWithCategory, MatchAll }.optional(), + SyntaxRule{ ConditionExpression, MatchAll }, + }; + + static constexpr SyntaxRule AndOrCondition[]{ + SyntaxRule{ AndOrTokens, MatchFirst, &LayerActionCondition::type }, + SyntaxRule{ SymbolType::NameWithCategory, MatchAll }, + SyntaxRule{ ConditionExpression, MatchAll }, + }; + + static auto internal_condition_matcher( + arctic::SyntaxRule const&, + arctic::MatchContext& ctx + ) noexcept -> arctic::ParseState; + + static constexpr SyntaxRule Rules[]{ + SyntaxRule{ internal_condition_matcher }.optional().repeat().noadvance() + }; + }; + + struct Modifier + { + static constexpr SyntaxRule Rule_LayerActionModifierComponentRules[]{ + SyntaxRule{ TokenType::CT_Dot, &LayerActionModifier::component }, + SyntaxRule{ TokenList::XYZ, MatchFirst, &LayerActionModifier::component, arctic::SyntaxRule::store_value_extend } + }; + + static constexpr SyntaxRule Rule_LayerActionModifierOperationListRules[]{ + SyntaxRule{ TokenType::OP_Div }, + SyntaxRule{ TokenType::OP_Mul }, + SyntaxRule{ TokenType::OP_Plus }, + SyntaxRule{ TokenType::OP_Minus }, + SyntaxRule{ TokenType::ASL_OP_Min }, + SyntaxRule{ TokenType::ASL_OP_Max }, + }; + + static constexpr SyntaxRule Rules[]{ + SyntaxRule{ TokenType::ASL_KW_Modifier }, + SyntaxRule{ Rule_LayerActionModifierComponentRules, MatchAll }, + SyntaxRule{ Rule_LayerActionModifierOperationListRules, MatchFirst, &LayerActionModifier::operation }, + SyntaxRule{ TokenList::NumberTypes, MatchFirst, &LayerActionModifier::param }, + SyntaxRule{ TokenType::ST_EndOfLine }, + }; + }; + + struct Action + { + static constexpr SyntaxRule ActionFlagList[]{ + SyntaxRule{ TokenType::ASL_KWF_Once, &LayerAction::flag_once }, + SyntaxRule{ TokenType::ASL_KWF_Toggled, &LayerAction::flag_toggled }, + }; + + static constexpr SyntaxRule ActionFlags[]{ + SyntaxRule{ TokenType::CT_Comma }, + SyntaxRule{ ActionFlagList, MatchFirst }, + }; + + static constexpr SyntaxRule Rules[]{ + SyntaxRule{ TokenType::ASL_KW_Action }, + SyntaxRule{ TokenType::CT_Symbol, &LayerAction::name }, + SyntaxRule{ TokenType::CT_Colon }, + SyntaxRule{ TokenList::ActionTypes, MatchFirst, &LayerAction::type }, + SyntaxRule{ ActionFlags, MatchAll }.optional(), + SyntaxRule{ TokenType::ST_EndOfLine }.repeat(), + SyntaxRule{ Condition::Rules, MatchAll }.optional(), + SyntaxRule{ Modifier::Rules, MatchChild }.repeat().optional(), + }; + }; + + struct Definition + { + static constexpr SyntaxRule LayerSourceRules[]{ + SyntaxRule{ TokenType::ASL_KW_Source }, + SyntaxRule{ TokenList::InputNativeTypes, MatchFirst, &LayerSource::type }, + SyntaxRule{ TokenType::CT_Symbol, &LayerSource::name }, + SyntaxRule{ SourceBinding::Rules, MatchChild }.optional(), + SyntaxRule{ TokenType::ST_EndOfLine }.repeat(), + }; + + static constexpr SyntaxRule LayerRules[]{ + SyntaxRule{ TokenType::ASL_KW_Layer }, + SyntaxRule{ TokenType::CT_Symbol, &Layer::name }, + SyntaxRule{ TokenType::CT_Colon }, + SyntaxRule{ TokenType::ST_EndOfLine }.repeat(), + SyntaxRule{ Constant::Rules, MatchChild }.optional().repeat(), + SyntaxRule{ LayerSourceRules, MatchChild }.optional().repeat(), + SyntaxRule{ Action::Rules, MatchChild }.optional().repeat(), + }; + }; + + static constexpr SyntaxRule ScriptRules[]{ + SyntaxRule{ Definition::LayerRules, MatchSibling }, + SyntaxRule{ TokenType::ST_EndOfLine }, // consume any outstanding newlines + }; + +} // namespace ice::grammar diff --git a/source/code/systems/input_action_system/private/input_action_script_parser.cxx b/source/code/systems/input_action_system/private/input_action_script_parser.cxx new file mode 100644 index 00000000..8bc2f14e --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script_parser.cxx @@ -0,0 +1,482 @@ +#include "input_action_script_grammar.hxx" +#include "input_action_script_parser.hxx" + +#include +#include +#include + +namespace ice +{ + + using TokenType = ice::asl::TokenType; + + namespace detail + { + + auto key_from_dsl(arctic::String value) noexcept -> ice::input::KeyboardKey; + auto mouse_from_dsl(arctic::String value) noexcept -> ice::input::MouseInput; + + auto datatype_from_dsl(arctic::Token token) noexcept -> ice::InputActionDataType; + auto condition_from_dsl(arctic::Token token, bool action_condition) noexcept -> ice::InputActionCondition; + auto step_from_dsl(arctic::Token token) noexcept -> ice::InputActionStep; + auto modifier_from_dsl(arctic::Token token) noexcept -> ice::InputActionModifier; + + } // namespace detail + + InputActionScriptParser::InputActionScriptParser(ice::Allocator& alloc) noexcept + : ActionInputParserEvents{ } + , _allocator{ alloc } + { } + + void InputActionScriptParser::visit(arctic::SyntaxNode node) noexcept + { + ICE_LOG(LogSeverity::Info, LogTag::Engine, "Layer: {}", node.data().name); + + ice::UniquePtr builder = ice::create_input_action_layer_builder( + _allocator, node.data().name + ); + + arctic::SyntaxNode<> child = node.child(); + while (child != false) + { + if (child.type() == ice::asl::SyntaxEntity::ASL_D_LayerSource) + { + visit_source(*builder, child.to()); + } + else if (child.type() == ice::asl::SyntaxEntity::ASL_D_LayerAction) + { + visit_action(*builder, child.to()); + } + else if (child.type() == ice::asl::SyntaxEntity::ASL_D_LayerConstant) + { + visit_constant(*builder, child.to()); + } + child = child.sibling(); + } + + ice::UniquePtr layer = builder->finalize(_allocator); + if (layer != nullptr) + { + on_layer_parsed(ice::move(layer)); + } + } + + void InputActionScriptParser::visit_constant( + ice::InputActionBuilder::Layer& layer, + arctic::SyntaxNode node + ) noexcept + { + ice::asl::LayerConstant const& data = node.data(); + ICE_LOG(LogSeverity::Debug, LogTag::Core, "Constant '{}' = {}", data.name, data.param.value); + + ice::f32 param; + if (ice::from_chars(data.param.value, param) == false) + { + return; + } + + if (ice::compare(data.name, "axis.deadzone") == ice::CompareResult::Equal) + { + layer.set_constant(InputActionConstant::ControllerAxisDeadzone, param); + } + } + + void InputActionScriptParser::visit_source( + ice::InputActionBuilder::Layer& layer, + arctic::SyntaxNode node + ) noexcept + { + ice::InputActionSourceType type = InputActionSourceType::Key; + switch(node.data().type.type) + { + case TokenType::ASL_NT_Button: type = InputActionSourceType::Button; break; + case TokenType::ASL_NT_Axis1D: type = InputActionSourceType::Axis1d; break; + case TokenType::ASL_NT_Axis2D: type = InputActionSourceType::Axis2d; break; + default: ICE_ASSERT_CORE(false); type = InputActionSourceType::Axis2d; break; + } + + ice::InputActionBuilder::Source source = layer.define_source(node.data().name, type); + arctic::SyntaxNode binding = node.child(); + while (binding) + { + using ice::input::DeviceType; + using ice::input::KeyboardKey; + using ice::input::KeyboardMod; + using ice::input::MouseInput; + + if (binding.data().device.type == TokenType::ASL_KW_Keyboard) + { + ICE_ASSERT_CORE(type == InputActionSourceType::Key || type == InputActionSourceType::Button); + + KeyboardKey const key = detail::key_from_dsl(binding.data().source); + ICE_ASSERT_CORE(key != KeyboardKey::Unknown); + source.add_key(key); + } + else if (binding.data().device.type == TokenType::ASL_KW_Mouse) + { + MouseInput const mouseinput = detail::mouse_from_dsl(binding.data().source); + ICE_ASSERT_CORE( + (type == InputActionSourceType::Key || type == InputActionSourceType::Button) + || mouseinput == MouseInput::PositionX + ); + + if (type == InputActionSourceType::Axis2d) + { + source.add_axis(mouseinput); + } + else + { + source.add_button(mouseinput); + } + } + + // TODO: We need to fix building layers with sources having multiple inputs, before enabling this. + binding = binding.sibling(); + } + + ICE_LOG(LogSeverity::Info, LogTag::Engine, "Source: {}", node.data().name); + } + + void InputActionScriptParser::visit_action( + ice::InputActionBuilder::Layer& layer, + arctic::SyntaxNode node + ) noexcept + { + ice::asl::LayerAction const& action_info = node.data(); + ice::InputActionDataType const action_datatype = detail::datatype_from_dsl(action_info.type); + + ICE_LOG(LogSeverity::Info, LogTag::Engine, "Action: {}", action_info.name); + ice::InputActionBuilder::Action action = layer.define_action(action_info.name, action_datatype); + ice::InputActionBehavior behavior = InputActionBehavior::Default; + if (action_info.flag_once) + { + behavior = InputActionBehavior::ActiveOnce; + } + else if (action_info.flag_toggled) + { + behavior = InputActionBehavior::Toggled; + } + //else if (action_info.flag_accumulated) + //{ + // behavior = InputActionBehavior::Accumulated; + //} + action.set_behavior(behavior); + + arctic::SyntaxNode<> child = node.child(); + while(child != false) + { + if (child.type() == ice::asl::SyntaxEntity::ASL_E_LayerActionCondition) + { + // Returns the final node of the series. The next sibling is either a: modifier, condition_series or action + child = visit_cond(action.add_condition_series(), child.to()); + } + else if (child.type() == ice::asl::SyntaxEntity::ASL_E_LayerActionModifier) + { + visit_mod(action, child.to()); + } + child = child.sibling(); + } + } + + auto InputActionScriptParser::visit_cond( + ice::InputActionBuilder::ConditionSeries series, + arctic::SyntaxNode node + ) noexcept -> arctic::SyntaxNode<> + { + using enum ice::InputActionCondition; + + arctic::SyntaxNode cond_node = node; + ICE_ASSERT_CORE(cond_node.data().type.type == TokenType::ASL_KW_When); + + ICE_LOG(LogSeverity::Debug, LogTag::Tool, "New condition series"); + do + { + node = cond_node; // Set the node to the current cond_node, necessary to return a valid node + ice::asl::LayerActionCondition const& cond = cond_node.data(); + ICE_LOG(LogSeverity::Debug, LogTag::Tool, "- {} {}.{} {} {}", cond.type.value, cond.source_type.value, cond.source_name, cond.condition.value, cond.param.value); + + bool const from_action = cond.source_type.type == TokenType::ASL_KW_Action + || (cond.source_type.type == arctic::TokenType::Invalid && cond.source_name.empty()); // "self" + ice::InputActionCondition const condition = detail::condition_from_dsl(cond.condition, from_action); + + ice::InputActionConditionFlags flags = InputActionConditionFlags::None; + if (cond_node.child()) + { + flags |= InputActionConditionFlags::RunSteps; + } + if (cond.flag_series) + { + flags |= InputActionConditionFlags::SeriesCheck; + } + + if (cond.type.type == TokenType::ASL_KW_WhenAnd) + { + flags |= InputActionConditionFlags::SeriesAnd; + } + // If we are When or WhenOr, we default to 'SeriesOr' + else + { + flags |= InputActionConditionFlags::SeriesOr; + } + + ice::f32 param = 0.0f; + bool const is_param_condition = condition == Equal || condition == NotEqual + || condition == Greater || condition == GreaterOrEqual + || condition == Lower || condition == LowerOrEqual; + if (is_param_condition) + { + ice::from_chars(cond.param.value, param); + } + + // Adds the new condition + series.add_condition( + cond.source_name, condition, flags, param, from_action + ); + + // Move over all steps defined for this condition + arctic::SyntaxNode<> steps = cond_node.child(); + while (steps != false) + { + if (steps.type() == ice::asl::SyntaxEntity::ASL_E_LayerActionStep) + { + visit_step(series, steps.to()); + } + steps = steps.sibling(); + } + + cond_node = cond_node.sibling(); + } while (cond_node && cond_node.data().type.type != TokenType::ASL_KW_When); + + // TODO: Check for '.continue' flag to skip setting 'Final' flag here + //if (/* DOES NOT HAVE '.continue' flag */) + { + series.set_finished(); + } + + return node; + } + + void InputActionScriptParser::visit_step( + ice::InputActionBuilder::ConditionSeries& condition_series, + arctic::SyntaxNode node + ) noexcept + { + ice::asl::LayerActionStep const& info = node.data(); + ice::InputActionStep const step = detail::step_from_dsl(info.step); + if (step == InputActionStep::Set || step == InputActionStep::Add || step == InputActionStep::Sub) + { + condition_series.add_step(info.source, step, info.destination); + } + else + { + condition_series.add_step(step); + } + } + + void InputActionScriptParser::visit_mod( + ice::InputActionBuilder::Action& action, + arctic::SyntaxNode node + ) noexcept + { + ice::asl::LayerActionModifier const& info = node.data(); + ice::InputActionModifier const modifier = detail::modifier_from_dsl(info.operation); + + ice::f32 param_value = 0.0f; + if (ice::from_chars(info.param, param_value) == false) + { + ICE_ASSERT_CORE(false); + } + + action.add_modifier(modifier, param_value, info.component); + } + + auto detail::key_from_dsl(arctic::String value) noexcept -> ice::input::KeyboardKey + { + using ice::input::KeyboardKey; + using ice::input::KeyboardMod; + + KeyboardKey result = KeyboardKey::Unknown; + if (value.size() == 1) + { + if (value[0] >= 'a' && value[0] <= 'z') + { + char const key_diff = value[0] - 'a'; + result = static_cast(static_cast(KeyboardKey::KeyA) + key_diff); + } + else if (value[0] >= 'A' && value[0] <= 'Z') + { + ICE_ASSERT_CORE(value[0] <= 'Z'); + char const key_diff = value[0] - 'A'; + + result = static_cast(static_cast(KeyboardKey::KeyA) + key_diff); + } + else if (value[0] >= '0' && value[0] <= '9') + { + char const key_diff = value[0] - '0'; + result = static_cast(static_cast(KeyboardKey::Key0) + key_diff); + } + } + else if (value.size() == 2 && (value[0] == 'f' || value[0] == 'F')) + { + if (value[1] >= '1' && value[1] <= '9') + { + char const key_diff = value[1] - '1'; + result = static_cast(static_cast(KeyboardKey::KeyF1) + key_diff); + } + } + else if (value.size() == 3) + { + if ((value[0] == 'f' || value[0] == 'F') && value[1] == '1' && value[2] >= '0' && value[2] <= '2') + { + char const key_diff = value[2] - '0'; + result = static_cast(static_cast(KeyboardKey::KeyF10) + key_diff); + } + } + else if (ice::compare(value, "space") == CompareResult::Equal) + { + result = KeyboardKey::Space; + } + else if (ice::compare(value, "up") == CompareResult::Equal) + { + result = KeyboardKey::Up; + } + else if (ice::compare(value, "down") == CompareResult::Equal) + { + result = KeyboardKey::Down; + } + else if (ice::compare(value, "left") == CompareResult::Equal) + { + result = KeyboardKey::Left; + } + else if (ice::compare(value, "right") == CompareResult::Equal) + { + result = KeyboardKey::Right; + } + else if (ice::compare(value, "mode") == CompareResult::Equal) + { + return KeyboardKey::KeyMode; + } + else if (ice::compare(value, "numlock") == CompareResult::Equal) + { + return KeyboardKey::NumPadNumlockClear; + } + else if (ice::compare(value, "capslock") == CompareResult::Equal) + { + return KeyboardKey::KeyCapsLock; + } + else if (value.size() >= 4 && (value[0] == 'l' || value[0] == 'r')) + { + ice::u8 const mod_left = value[0] == 'l'; + value = value.substr(1); + + if (ice::compare(value, "shift") == CompareResult::Equal) + { + result = mod_left ? KeyboardKey::KeyLeftShift : KeyboardKey::KeyRightShift; + } + else if (ice::compare(value, "ctrl") == CompareResult::Equal) + { + result = mod_left ? KeyboardKey::KeyLeftCtrl : KeyboardKey::KeyRightCtrl; + } + else if (ice::compare(value, "alt") == CompareResult::Equal) + { + result = mod_left ? KeyboardKey::KeyLeftAlt : KeyboardKey::KeyRightAlt; + } + else if (ice::compare(value, "gui") == CompareResult::Equal) + { + result = mod_left ? KeyboardKey::KeyLeftGui : KeyboardKey::KeyRightGui; + } + } + return result; + } + + auto detail::mouse_from_dsl(arctic::String value) noexcept -> ice::input::MouseInput + { + using ice::input::MouseInput; + MouseInput result = MouseInput::Unknown; + if (value == "lbutton") + { + result = MouseInput::ButtonLeft; + } + else if (value == "rbutton") + { + result = MouseInput::ButtonRight; + } + else if (value == "mbutton") + { + result = MouseInput::ButtonMiddle; + } + else if (value == "pos" || value == "position") + { + result = MouseInput::PositionX; + } + return result; + } + + auto detail::datatype_from_dsl(arctic::Token token) noexcept -> ice::InputActionDataType + { + switch (token.type) + { + // Action conditions + case TokenType::ASL_NT_Bool: return InputActionDataType::Bool; + case TokenType::ASL_NT_Float1: return InputActionDataType::Float1; + case TokenType::ASL_NT_Float2: return InputActionDataType::Float2; + case TokenType::ASL_NT_Object: return InputActionDataType::ActionObject; + default: ICE_ASSERT_CORE(false); return InputActionDataType::Invalid; + } + } + + auto detail::condition_from_dsl(arctic::Token token, bool action_condition) noexcept -> ice::InputActionCondition + { + switch(token.type) + { + // Action conditions + case TokenType::ASL_OP_IsPressed: return InputActionCondition::Pressed; + case TokenType::ASL_OP_IsReleased: return InputActionCondition::Released; + case TokenType::ASL_OP_IsActive: return action_condition ? InputActionCondition::ActionToggleActive : InputActionCondition::Active; + case TokenType::ASL_OP_IsInactive: return InputActionCondition::ActionToggleInactive; + // Arithmetic conditions + case TokenType::OP_Equal: return InputActionCondition::Equal; + case TokenType::OP_NotEqual: return InputActionCondition::NotEqual; // TODO + case TokenType::OP_Greater: return InputActionCondition::Greater; + case TokenType::OP_GreaterOrEqual: return InputActionCondition::GreaterOrEqual; + case TokenType::OP_Less: return InputActionCondition::Lower; + case TokenType::OP_LessOrEqual: return InputActionCondition::LowerOrEqual; + // Special conditions + case TokenType::KW_True: return InputActionCondition::AlwaysTrue; + default: ICE_ASSERT_CORE(false); return InputActionCondition::Invalid; + } + } + + auto detail::step_from_dsl(arctic::Token token) noexcept -> ice::InputActionStep + { + switch(token.type) + { + case TokenType::ASL_OP_Activate: return InputActionStep::Activate; + case TokenType::ASL_OP_Deactivate: return InputActionStep::Deactivate; + case TokenType::ASL_OP_Toggle: return InputActionStep::Toggle; + case TokenType::ASL_OP_Reset: return InputActionStep::Reset; + case TokenType::ASL_OP_Time: return InputActionStep::Time; + // Arithmetic steps + case TokenType::OP_Assign: return InputActionStep::Set; + case TokenType::OP_Plus: return InputActionStep::Add; + case TokenType::OP_Minus: return InputActionStep::Sub; + default: ICE_ASSERT_CORE(false); break; + } + return InputActionStep::Invalid; + } + + auto detail::modifier_from_dsl(arctic::Token token) noexcept -> ice::InputActionModifier + { + switch (token.type) + { + case TokenType::OP_Plus: return InputActionModifier::Add; + case TokenType::OP_Minus: return InputActionModifier::Sub; + case TokenType::OP_Mul: return InputActionModifier::Mul; + case TokenType::OP_Div: return InputActionModifier::Div; + case TokenType::ASL_OP_Max: return InputActionModifier::MaxOf; + case TokenType::ASL_OP_Min: return InputActionModifier::MinOf; + default: ICE_ASSERT_CORE("Not Implemented!" && false); return InputActionModifier::Invalid; + } + } + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_script_parser.hxx b/source/code/systems/input_action_system/private/input_action_script_parser.hxx new file mode 100644 index 00000000..629d2fff --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script_parser.hxx @@ -0,0 +1,52 @@ +#pragma once +#include "input_action_script.hxx" +#include + +namespace ice +{ + + class InputActionScriptParser : public ice::asl::ActionInputParserEvents + { + public: + InputActionScriptParser(ice::Allocator& alloc) noexcept; + + virtual void on_layer_parsed(ice::UniquePtr layer) noexcept = 0; + + void visit(arctic::SyntaxNode node) noexcept override; + + private: + void visit_constant( + ice::InputActionBuilder::Layer& layer, + arctic::SyntaxNode node + ) noexcept; + + void visit_source( + ice::InputActionBuilder::Layer& layer, + arctic::SyntaxNode node + ) noexcept; + + void visit_action( + ice::InputActionBuilder::Layer& layer, + arctic::SyntaxNode node + ) noexcept; + + auto visit_cond( + ice::InputActionBuilder::ConditionSeries series, + arctic::SyntaxNode node + ) noexcept -> arctic::SyntaxNode<>; + + void visit_step( + ice::InputActionBuilder::ConditionSeries& condition_series, + arctic::SyntaxNode node + ) noexcept; + + void visit_mod( + ice::InputActionBuilder::Action& action, + arctic::SyntaxNode node + ) noexcept; + + private: + ice::Allocator& _allocator; + }; + +} // namespace ice diff --git a/source/code/systems/input_action_system/private/input_action_script_syntax_data.hxx b/source/code/systems/input_action_system/private/input_action_script_syntax_data.hxx new file mode 100644 index 00000000..1f2e79bc --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script_syntax_data.hxx @@ -0,0 +1,108 @@ +#pragma once +#include +#include +#include +#include + +namespace ice::asl +{ + + static constexpr ice::u32 ASLE_Base = ice::u32(arctic::SyntaxEntity::E_CallArg) + 1; + static constexpr ice::u32 ASLE_Definitions = ASLE_Base + 0; + static constexpr ice::u32 ASLE_Expressions = ASLE_Base + 100; + + using arctic::SyntaxNodeData; + + struct SyntaxEntity + { + static constexpr arctic::SyntaxEntity ASL_D_Layer{ ASLE_Definitions + 0 }; + static constexpr arctic::SyntaxEntity ASL_D_LayerConstant{ ASLE_Definitions + 1 }; + static constexpr arctic::SyntaxEntity ASL_D_LayerSource{ ASLE_Definitions + 2 }; + static constexpr arctic::SyntaxEntity ASL_D_LayerAction{ ASLE_Definitions + 3 }; + + static constexpr arctic::SyntaxEntity ASL_E_LayerSourceBinding{ ASLE_Expressions + 0 }; + static constexpr arctic::SyntaxEntity ASL_E_LayerActionStep{ ASLE_Expressions + 1 }; + static constexpr arctic::SyntaxEntity ASL_E_LayerActionCondition{ ASLE_Definitions + 2 }; + static constexpr arctic::SyntaxEntity ASL_E_LayerActionModifier{ ASLE_Definitions + 3 }; + }; + +#define NodeData(type) \ + static constexpr arctic::SyntaxEntity RepresentedSyntaxEntity = SyntaxEntity::type; \ + using arctic::SyntaxNodeData::SyntaxNodeData + + struct Layer : arctic::SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_D_Layer); + + arctic::String name; + }; + + struct LayerConstant : SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_D_LayerConstant); + + arctic::String name; + arctic::Token param; + }; + + struct LayerSource : SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_D_LayerSource); + + arctic::Token type; + arctic::String name; + }; + + struct LayerAction : SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_D_LayerAction); + + arctic::String name; + arctic::Token type; + bool flag_toggled = false; + bool flag_once = false; + }; + + struct LayerSourceBinding : SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_E_LayerSourceBinding); + + arctic::Token device; + arctic::String source; + }; + + struct LayerActionCondition : SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_E_LayerActionCondition); + + arctic::Token type; // when/and(_when)/or(_when) + arctic::Token source_type = {}; // source/action + arctic::String source_name; // alpha-num[.x/y/z] + //arctic::String source_component; // x/y/z + arctic::Token condition; // .pressed/released/active/inactive//==/>=/<=/!= + arctic::Token param; // int/float + bool flag_series = false; + }; + + struct LayerActionStep : SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_E_LayerActionStep); + + arctic::Token source_type; + arctic::String source; + arctic::String destination; + arctic::Token step; + }; + + struct LayerActionModifier : SyntaxNodeData + { + NodeData(SyntaxEntity::ASL_E_LayerActionModifier); + + arctic::String component; + arctic::Token operation; + arctic::String param; + }; + +#undef NodeData + +} // namespace ice::syntax diff --git a/source/code/systems/input_action_system/private/input_action_script_tokens.hxx b/source/code/systems/input_action_system/private/input_action_script_tokens.hxx new file mode 100644 index 00000000..79a008e1 --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_script_tokens.hxx @@ -0,0 +1,133 @@ +#pragma once +#include +#include + +namespace ice::asl +{ + + static constexpr ice::u32 ASLT_Base = ice::u32(arctic::TokenType::ST_Any) + 1; + static constexpr ice::u32 ASLT_Keywords = ASLT_Base + 1000; + static constexpr ice::u32 ASLT_Operators = ASLT_Base + 2000; + static constexpr ice::u32 ASLT_NativeTypes = ASLT_Base + 3000; + + struct TokenType + { + using enum arctic::TokenType; + + // General ASL keywords + static constexpr arctic::TokenType ASL_KW_Layer{ ASLT_Keywords + 0 }; + static constexpr arctic::TokenType ASL_KW_Constant{ ASLT_Keywords + 1 }; + static constexpr arctic::TokenType ASL_KW_Source{ ASLT_Keywords + 2 }; + static constexpr arctic::TokenType ASL_KW_Action{ ASLT_Keywords + 3 }; + static constexpr arctic::TokenType ASL_KW_Modifier{ ASLT_Keywords + 4 }; + + static constexpr arctic::TokenType ASL_KW_When{ ASLT_Keywords + 10 }; + static constexpr arctic::TokenType ASL_KW_WhenAnd{ ASLT_Keywords + 11 }; + static constexpr arctic::TokenType ASL_KW_WhenOr{ ASLT_Keywords + 12 }; + + // Keywords for Device types + static constexpr arctic::TokenType ASL_KW_Mouse{ ASLT_Keywords + 20 }; + static constexpr arctic::TokenType ASL_KW_Keyboard{ ASLT_Keywords + 21 }; + static constexpr arctic::TokenType ASL_KW_Controller{ ASLT_Keywords + 22 }; + + // Keywords for Action and Condition flags + static constexpr arctic::TokenType ASL_KWF_Once{ ASLT_Keywords + 30 }; + static constexpr arctic::TokenType ASL_KWF_Toggled{ ASLT_Keywords + 31 }; + static constexpr arctic::TokenType ASL_KWF_CheckSeries{ ASLT_Keywords + 32 }; + + // Condition operators (checks) + static constexpr arctic::TokenType ASL_OP_IsPressed{ ASLT_Operators + 0 }; + static constexpr arctic::TokenType ASL_OP_IsReleased{ ASLT_Operators + 1 }; + static constexpr arctic::TokenType ASL_OP_IsActive{ ASLT_Operators + 2 }; + static constexpr arctic::TokenType ASL_OP_IsInactive{ ASLT_Operators + 3 }; + + // Step operators (actions) + static constexpr arctic::TokenType ASL_OP_Activate{ ASLT_Operators + 10 }; + static constexpr arctic::TokenType ASL_OP_Deactivate{ ASLT_Operators + 11 }; + static constexpr arctic::TokenType ASL_OP_Toggle{ ASLT_Operators + 12 }; + static constexpr arctic::TokenType ASL_OP_Reset{ ASLT_Operators + 13 }; + static constexpr arctic::TokenType ASL_OP_Time{ ASLT_Operators + 14 }; + + // Modifier operators + static constexpr arctic::TokenType ASL_OP_Min{ ASLT_Operators + 20 }; + static constexpr arctic::TokenType ASL_OP_Max{ ASLT_Operators + 21 }; + + // Get axis component operators + static constexpr arctic::TokenType ASL_OP_CmpX{ ASLT_Operators + 30 }; + static constexpr arctic::TokenType ASL_OP_CmpY{ ASLT_Operators + 31 }; + static constexpr arctic::TokenType ASL_OP_CmpZ{ ASLT_Operators + 32 }; + + // Native types representing input source + static constexpr arctic::TokenType ASL_NT_Axis1D{ ASLT_NativeTypes + 0 }; + static constexpr arctic::TokenType ASL_NT_Axis2D{ ASLT_NativeTypes + 1 }; + static constexpr arctic::TokenType ASL_NT_Axis3D{ ASLT_NativeTypes + 2 }; + static constexpr arctic::TokenType ASL_NT_Button{ ASLT_NativeTypes + 3 }; + + // Native types representing outputs + static constexpr arctic::TokenType ASL_NT_Bool{ ASLT_NativeTypes + 10 }; + static constexpr arctic::TokenType ASL_NT_Float1{ ASLT_NativeTypes + 11 }; + static constexpr arctic::TokenType ASL_NT_Float2{ ASLT_NativeTypes + 12 }; + static constexpr arctic::TokenType ASL_NT_Float3{ ASLT_NativeTypes + 13 }; + static constexpr arctic::TokenType ASL_NT_Object{ ASLT_NativeTypes + 14 }; + }; + + struct TokenDefinition + { + arctic::TokenType type; + arctic::String value; + }; + + static constexpr std::array Constant_TokenDefinitionsUnsorted{ + TokenDefinition{ TokenType::ASL_KW_Action, "action" }, + TokenDefinition{ TokenType::ASL_KW_Constant, "constant" }, + TokenDefinition{ TokenType::ASL_KW_Controller, "gamepad" }, + TokenDefinition{ TokenType::ASL_KW_Controller, "gp" }, + TokenDefinition{ TokenType::ASL_KW_Keyboard, "kb" }, + TokenDefinition{ TokenType::ASL_KW_Keyboard, "key" }, + TokenDefinition{ TokenType::ASL_KW_Layer, "layer" }, + TokenDefinition{ TokenType::ASL_KW_Modifier, "mod" }, + TokenDefinition{ TokenType::ASL_KW_Mouse, "mouse" }, + TokenDefinition{ TokenType::ASL_KW_Mouse, "mp" }, + TokenDefinition{ TokenType::ASL_KW_Source, "source" }, + TokenDefinition{ TokenType::ASL_KW_When, "when" }, + TokenDefinition{ TokenType::ASL_KW_WhenAnd, "and" }, + TokenDefinition{ TokenType::ASL_KW_WhenOr, "or" }, + TokenDefinition{ TokenType::ASL_KWF_CheckSeries, "series" }, + TokenDefinition{ TokenType::ASL_KWF_Once, "once" }, + TokenDefinition{ TokenType::ASL_KWF_Toggled, "toggled" }, + TokenDefinition{ TokenType::ASL_NT_Axis1D, "axis1d" }, + TokenDefinition{ TokenType::ASL_NT_Axis2D, "axis2d" }, + TokenDefinition{ TokenType::ASL_NT_Axis3D, "axis3d" }, + TokenDefinition{ TokenType::ASL_NT_Bool, "bool" }, + TokenDefinition{ TokenType::ASL_NT_Button, "button" }, + TokenDefinition{ TokenType::ASL_NT_Float1, "float1" }, + TokenDefinition{ TokenType::ASL_NT_Float2, "float2" }, + TokenDefinition{ TokenType::ASL_NT_Float3, "float3" }, + TokenDefinition{ TokenType::ASL_NT_Object, "object" }, + TokenDefinition{ TokenType::ASL_OP_Activate, "activate" }, + TokenDefinition{ TokenType::ASL_OP_Deactivate, "deactivate" }, + TokenDefinition{ TokenType::ASL_OP_IsActive, "active" }, + TokenDefinition{ TokenType::ASL_OP_IsInactive, "inactive" }, + TokenDefinition{ TokenType::ASL_OP_IsPressed, "pressed" }, + TokenDefinition{ TokenType::ASL_OP_IsReleased, "released" }, + TokenDefinition{ TokenType::ASL_OP_Max, "max" }, + TokenDefinition{ TokenType::ASL_OP_Min, "min" }, + TokenDefinition{ TokenType::ASL_OP_Reset, "reset" }, + TokenDefinition{ TokenType::ASL_OP_Time, "time" }, + TokenDefinition{ TokenType::ASL_OP_Toggle, "toggle" }, + }; + + constexpr auto operator<=>(ice::asl::TokenDefinition left, ice::asl::TokenDefinition right) noexcept + { + return left.value <=> right.value; + } + + constexpr bool operator==(ice::asl::TokenDefinition left, ice::asl::TokenDefinition right) noexcept + { + return left.value == right.value; + } + + static constexpr std::array Constant_TokenDefinitions + = ice::constexpr_sort_stdarray(Constant_TokenDefinitionsUnsorted, 0); + +} // namespace ice::asl diff --git a/source/code/systems/input_action_system/private/input_action_stack.cxx b/source/code/systems/input_action_system/private/input_action_stack.cxx new file mode 100644 index 00000000..e374b089 --- /dev/null +++ b/source/code/systems/input_action_system/private/input_action_stack.cxx @@ -0,0 +1,521 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ice +{ + + class SimpleInputActionStack final : public InputActionStack + { + public: + SimpleInputActionStack( + ice::Allocator& alloc, + ice::String actionid_prefix + ) noexcept + : _allocator{ alloc } + , _idprefix{ _allocator, actionid_prefix } + , _layers{ _allocator } + , _layers_active{ _allocator } + , _layers_sources_indices{ _allocator } + , _sources_runtime_values{ _allocator } + , _sources{ _allocator } + , _actions{ _allocator } + , _action_names{ _allocator } + { + } + + auto registered_layers( + ice::Array& out_layers + ) const noexcept -> ice::ucount override; + + auto register_layer( + ice::InputActionLayer const* layer + ) noexcept -> ice::Result override; + + + auto active_layers( + ice::Array& out_layers + ) const noexcept -> ice::ucount override; + + void push_layer( + ice::InputActionLayer const* layer + ) noexcept override; + + void pop_layer( + ice::InputActionLayer const* layer + ) noexcept override; + + + auto action( + ice::String action + ) const noexcept -> ice::Expected override; + + bool action_check( + ice::String action, + ice::InputActionCheck check = InputActionCheck::Active + ) const noexcept override; + + auto action_time( + ice::String action + ) const noexcept -> ice::Tms override; + + bool action_value( + ice::String action, + ice::vec2f& out_value + ) const noexcept override; + + bool action_value( + ice::String action, + ice::vec2f& out_value, + ice::Tns& out_timeactive + ) const noexcept override; + + void process_inputs(ice::Span events) noexcept override; + auto publish_shards(ice::ShardContainer& out_shards) const noexcept -> void override; + + + auto action_runtime( + ice::InputActionLayer const& layer, + ice::InputActionInfo const& action_info + ) const noexcept -> ice::InputActionRuntime const& override; + + auto source_runtime( + ice::InputActionLayer const& layer, + ice::InputActionSourceInputInfo const& source_info + ) const noexcept -> ice::InputActionSource const& override; + + private: + struct StackLayer + { + ice::InputActionLayer const* layer; + ice::ref16 sources_indices; + }; + + struct ActiveStackLayer + { + ice::u32 index; + }; + + struct StackSourceIdx + { + ice::u32 index; + }; + + // Helpers + static bool compare_layers(StackLayer const& slayer, ice::InputActionLayer const* layer) noexcept + { + return slayer.layer == layer; + } + + private: + ice::Allocator& _allocator; + ice::HeapString<> _idprefix; + + ice::Array _layers; + ice::Array _layers_active; + ice::Array _layers_sources_indices; + ice::Array _sources_runtime_values; + + ice::HashMap _sources; + ice::HashMap _actions; + ice::HashMap> _action_names; + }; + + auto SimpleInputActionStack::registered_layers( + ice::Array& out_layers + ) const noexcept -> ice::ucount + { + for (StackLayer const& layer : _layers) + { + ice::array::push_back(out_layers, layer.layer); + } + return ice::count(_layers); + } + + auto SimpleInputActionStack::register_layer( + ice::InputActionLayer const* layer + ) noexcept -> ice::Result + { + IPT_ZONE_SCOPED; + if (layer == nullptr) + { + return E_NullPointer; + } + + ice::ucount layer_idx; + if (ice::search(ice::Span{ _layers }, layer, compare_layers, layer_idx)) + { + return S_LayerAlreadyRegistered; + } + + layer_idx = ice::count(_layers); + + // We reserve enough space to store all actual indices for accessed layer sources. + ice::u32 const layer_sources_count = ice::count(layer->sources()); + ice::u32 const layer_sources_offset = ice::count(_layers_sources_indices); + ice::array::reserve(_layers_sources_indices, layer_sources_offset + layer_sources_count); + + // Go through each resource and set the indices + ice::u64 prev_name_hash = 0; + ice::Span sources = layer->sources(); + ice::ucount const count_sources = ice::count(sources); + for (ice::u32 idx = 0; idx < count_sources; ++idx) + { + ice::InputActionSourceInputInfo const& source = sources[idx]; + ice::String const source_name = layer->source_name(source); + ice::u64 const source_name_hash = ice::hash(source_name); + + // First we check if a source with a name like this already exists, if so we take the index to it's location + // else we push back a new value to the list of runtime values and store it's index under the hashed name. + ice::u32 values_index = 0; + if (ice::hashmap::has(_sources, source_name_hash)) + { + if (prev_name_hash != source_name_hash) + { + values_index = ice::hashmap::try_get(_sources, source_name_hash)->index; + + // Stores the index for the source + ice::array::push_back(_layers_sources_indices, values_index); + + if (source.type == InputActionSourceType::Axis2d) + { + // Stores the index for the source + ice::array::push_back(_layers_sources_indices, values_index); + } + } + } + else + { + values_index = ice::array::count(_sources_runtime_values); + + ice::array::push_back(_sources_runtime_values, InputActionSource{}); + // If we have an Axis2d source, we need to actually push back two values for both axis + if (source.type == InputActionSourceType::Axis2d) + { + ice::array::push_back(_sources_runtime_values, InputActionSource{}); + } + + // Save the index where we store the runtime value(s) + ice::hashmap::set(_sources, source_name_hash, { values_index }); + + // Stores the index for the source + ice::array::push_back(_layers_sources_indices, values_index); + + if (source.type == InputActionSourceType::Axis2d) + { + // Stores the index for the source + ice::array::push_back(_layers_sources_indices, values_index); + } + } + + prev_name_hash = source_name_hash; + } + + // Go through each resource and assing the pointers. + for (ice::InputActionInfo const& action : layer->actions()) + { + ice::String const action_name = layer->action_name(action); + ice::u64 const action_name_hash = ice::hash(action_name); + + if (ice::hashmap::has(_actions, action_name_hash) == false) + { + // Create the final name with the prefix and store it so the pointer reimains valid. + // #TODO: Consider using refs instead? + ice::HeapString<> final_name = _idprefix; + ice::string::push_back(final_name, action_name); + ice::hashmap::set(_action_names, action_name_hash, ice::move(final_name)); + + ice::HeapString<> const* final_name_ptr = ice::hashmap::try_get(_action_names, action_name_hash); + ICE_ASSERT_CORE(final_name_ptr != nullptr); + + // Save the pointer where the values are stored + ice::hashmap::set(_actions, action_name_hash, { .type = action.type, .name = *final_name_ptr }); + } + } + + // Stores the layer along with a ref where it's indices for all sources are stored. + ice::array::push_back( + _layers, + StackLayer{ layer, { ice::u16(layer_sources_offset), ice::u16(ice::count(_layers_sources_indices) - layer_sources_offset) } } + ); + return S_Ok; + } + + auto SimpleInputActionStack::active_layers( + ice::Array& out_layers + ) const noexcept -> ice::ucount + { + auto it = ice::array::rbegin(_layers_active); + auto const end = ice::array::rend(_layers_active); + + while (it != end) + { + ice::array::push_back(out_layers, _layers[it->index].layer); + it += 1; + } + return ice::count(_layers_active); + } + + void SimpleInputActionStack::push_layer( + ice::InputActionLayer const* layer + ) noexcept + { + ice::ucount idx; + if (ice::search(ice::Span{ _layers }, layer, compare_layers, idx) == false) + { + // #TODO: Create a proper error return value. + ICE_ASSERT_CORE(false); + } + + // Check if we are already at the top of the stack. + if (ice::array::any(_layers_active) && ice::array::back(_layers_active).index == idx) + { + return; // #TODO: Implement success with info. + } + + // Push back the new active layer. + ice::array::push_back(_layers_active, { idx }); + } + + void SimpleInputActionStack::pop_layer( + ice::InputActionLayer const* layer + ) noexcept + { + ice::ucount idx; + if (ice::search(ice::Span{ _layers }, layer, compare_layers, idx)) + { + // We just cut anything below this index, because we want to pop everything up to this layer + ice::array::resize(_layers, idx); + ice::array::pop_back(_layers); // And pop the item itself + } + } + + auto SimpleInputActionStack::action(ice::String action_name) const noexcept -> ice::Expected + { + ice::InputActionRuntime const* action = ice::hashmap::try_get(_actions, ice::hash(action_name)); + if (action == nullptr && ice::string::starts_with(action_name, _idprefix)) + { + // Try again after removing the prefix + action = ice::hashmap::try_get( + _actions, + ice::hash( + ice::string::substr(action_name, ice::size(_idprefix)) + ) + ); + } + if (action != nullptr) + { + if (action->enabled == false) + { + return E_InputActionDisabled; + } + else if (action->active == false) + { + return E_InputActionInactive; + } + else + { + return action; + } + } + return E_UnknownInputAction; + } + + bool SimpleInputActionStack::action_check( + ice::String action_name, + ice::InputActionCheck check + ) const noexcept + { + // We check for validity not for success. NOTE: 'operator bool()' checks for success. + if (ice::Expected const action = this->action(action_name); action.valid()) + { + switch (check) + { + using enum InputActionCheck; + case Exists: return true; + case Enabled: return action != E_InputActionDisabled; + case Disabled: return action == E_InputActionDisabled; + case Active: return action.succeeded(); + case Inactive: return action == E_InputActionInactive; + default: ICE_ASSERT_CORE(false); break; + } + } + return false; + } + + auto SimpleInputActionStack::action_time( + ice::String action_name + ) const noexcept -> ice::Tms + { + // Check for success + if (ice::Expected const action = this->action(action_name); action.succeeded()) + { + return (Tms) ice::clock::elapsed(action.value()->timestamp, ice::clock::now()); + } + return Tms{ }; + } + + bool SimpleInputActionStack::action_value( + ice::String action_name, + ice::vec2f& out_value + ) const noexcept + { + // Check for success + if (ice::Expected const action = this->action(action_name); action.succeeded()) + { + out_value = action.value()->value; + return true; + } + return false; + } + + bool SimpleInputActionStack::action_value( + ice::String action_name, + ice::vec2f& out_value, + ice::Tns& out_timeactive + ) const noexcept + { + // Check for success + if (ice::Expected const action = this->action(action_name); action.succeeded()) + { + out_value = action.value()->value; + out_timeactive = ice::clock::elapsed(action.value()->timestamp, ice::clock::now()); + return true; + } + return false; + } + + void SimpleInputActionStack::process_inputs( + ice::Span events + ) noexcept + { + IPT_ZONE_SCOPED; + using Iterator = ice::Array::ConstReverseIterator; + + ice::ucount remaining_events = ice::count(events); + ice::Array events_copy{ _allocator, events }; + ice::Array source_values{ _allocator }; + + // We go in reverse order since, the recently pushed layers should be processed first as they might override inputs. + Iterator const start = ice::array::rbegin(_layers_active); + Iterator const end = ice::array::rend(_layers_active); + + for (Iterator it = start; it != end; ++it) + { + StackLayer const& layer = _layers[it->index]; + for (ice::u32 offset : ice::array::slice(_layers_sources_indices, layer.sources_indices)) + { + ice::array::push_back(source_values, ice::addressof(_sources_runtime_values[offset])); + } + + ice::ucount const processed_events = layer.layer->process_inputs( + ice::array::slice(events_copy, 0, remaining_events), + source_values + ); + ICE_ASSERT_CORE(processed_events <= remaining_events); + remaining_events -= processed_events; + + ice::array::clear(source_values); + + // TODO: Should we change how this loop is finishing? + if (remaining_events == 0) + { + break; + } + } + + ice::InputActionExecutor ex{}; + for (Iterator it = start; it != end; ++it) + { + StackLayer const& layer = _layers[it->index]; + for (ice::u32 offset : ice::array::slice(_layers_sources_indices, layer.sources_indices)) + { + ice::array::push_back(source_values, ice::addressof(_sources_runtime_values[offset])); + } + + ex.prepare_constants(*layer.layer); + layer.layer->update_actions(ex, source_values, _actions); + + ice::array::clear(source_values); + } + } + + auto SimpleInputActionStack::publish_shards( + ice::ShardContainer& out_shards + ) const noexcept -> void + { + for (ice::InputActionRuntime const& action : _actions) + { + if (action.active) + { + ice::ShardID const sid = ice::shardid(action.name); + switch (action.type) + { + using enum InputActionDataType; + case Bool: + ice::shards::push_back(out_shards, sid | bool(action.value.x > 0.0f)); + break; + case Float1: + ice::shards::push_back(out_shards, sid | action.value.x); + break; + case Float2: + ice::shards::push_back(out_shards, sid | action.value); + break; + case ActionObject: + ice::shards::push_back(out_shards, sid | static_cast(ice::addressof(action))); + break; + default: ICE_ASSERT_CORE(false); break; + } + } + } + } + + auto SimpleInputActionStack::action_runtime( + ice::InputActionLayer const& layer, + ice::InputActionInfo const& action_info + ) const noexcept -> ice::InputActionRuntime const& + { + static ice::InputActionRuntime invalid{.name=""}; + ice::String const action_name = layer.action_name(action_info); + return ice::hashmap::get(_actions, ice::hash(action_name), invalid); + } + + auto SimpleInputActionStack::source_runtime( + ice::InputActionLayer const& layer, + ice::InputActionSourceInputInfo const& source_info + ) const noexcept -> ice::InputActionSource const& + { + static ice::InputActionSource invalid{}; + ice::String const source_name = layer.source_name(source_info); + + ice::u32 const values_idx = ice::hashmap::get( + _sources, + ice::hash(source_name), + {.index=ice::u32_max} + ).index; + + if (values_idx == ice::u32_max) + { + return invalid; + } + return _sources_runtime_values[values_idx]; + } + + auto create_input_action_stack( + ice::Allocator& alloc, + ice::String actionid_prefix + ) noexcept -> ice::UniquePtr + { + return ice::make_unique(alloc, alloc, actionid_prefix); + } + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action.hxx b/source/code/systems/input_action_system/public/ice/input_action.hxx new file mode 100644 index 00000000..928e8172 --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action.hxx @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include + +namespace ice +{ + + static constexpr ice::AssetCategory AssetCategory_InputActionsScript = ice::make_asset_category("ice/input_actions/script"); + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action_definitions.hxx b/source/code/systems/input_action_system/public/ice/input_action_definitions.hxx new file mode 100644 index 00000000..b074e3da --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action_definitions.hxx @@ -0,0 +1,287 @@ +#pragma once +#include +#include + +namespace ice +{ + + //! \brief Developer defined input action that can represent anything from a simple click, + //! down to specific axis values and mixes between them. + struct InputAction + { + //! \brief Provides a quick way to disable or enable specific actions during runtime. (default: true) + bool enabled = true; + + //! \brief Tracks activity of the action. (default: false) + //! \note An action can only be active if it's enabled. + bool active = false; + + //! \brief The value the action is returning. This value is defined / calculated by a set of steps + //! the developer created for this specific action. + //! \note The value is up-to-date / valid only when the action is active. + ice::vec2f value; + + //! \brief Timestamp when the action was activated. + //! \note The value is up-to-date / valid only when then action is active. + ice::Timestamp timestamp; + }; + + //! \brief Events input sources can be updated by. + enum class InputActionSourceEvent : ice::u8 + { + //! \brief Input source was not updated in this frame. + None, + + //! \brief Input source value was updated by a key press event. + KeyPress, + + //! \brief Input source value was updated by a key release event. + KeyRelease, + + //! \brief \copybrief KeyPress + //! \note Alias more suitable for controllers. + ButtonPress = KeyPress, + + //! \brief \copybrief KeyRelease + //! \note Alias more suitable for controllers. + ButtonRelease = KeyRelease, + + //! \brief Input source was updated by a controller trigger. (can represent any one-dimensional floating point input) + Trigger, + + //! \brief Input source was updated by a controller stick input. (can represent any two-dimensional floating point input) + //! \note This event will only be captured if the value is \b outside the deadzone. + Axis, + + //! \brief Input source was updated by a controller stick input. (can represent any two-dimensional floating point input) + //! \note This event will only be captured if the value is \b inside the deadzone. + AxisDeadzone + }; + + //! \brief Developer defined "input source" for activating actions and processing their values. + //! \details A source represents a single device input event. This can be a Button, Axis, Trigger, Key, Position on + //! any input device like a mouse, keyboard, controller or anything else. + //! + //! \note When represeting an axis, only a single component is set each source. + //! So for a 2d axis you will need to define two sources. + //! \todo Try to remove the 23bit padding. + struct InputActionSource + { + //! \brief The value of the input source. + //! \details See \see InputActionSourceInfo::type for details. + ice::f32 value; + + //! \brief The most recent event that triggered a value change for this source. + //! \note It might be possible for multiple events of different types, to trigger an event. + ice::InputActionSourceEvent temp_event; + + //! \brief The final type of the event that triggered a value change for this source. + //! \note It might be possible for multiple events of different types, to trigger an event. + ice::InputActionSourceEvent event; + + // 2 bytes of padding + }; + + static_assert(sizeof(InputActionSource) == 8); + + //! \brief Runtime representation of an action, required to handle internal state and value changes. + struct InputActionRuntime : ice::InputAction + { + //! \copydoc InputActionInfo::type + ice::InputActionDataType type; + + //! \brief Internal state value used to track action activation changes. + ice::u8 state = 0; + + //! \brief Tracks if the action is currently enabled. (only for toggled actions) + bool toggle_enabled = false; + + //! \brief Tracks if an action was enabled in the previous frame. + bool was_active = false; + + //! \brief The value stored before modifiers are applied. + ice::vec3f raw_value; + + //! \brief Final name of the action, combined with the 'stack' prefix. + ice::String name; + }; + + //! \brief Conditions that can be executed by an action. + enum class InputActionCondition : ice::u8 + { + Invalid = 0, + + //! \brief Source is active. The source was updated by an input event this frame. + Active, + + //! \brief Source was updated by a key / button press. + Pressed, + + //! \brief Source was updated by a key / button release. + Released, + + //! \brief Source was updated by a change in the reported trigger value. + Trigger, + + //! \brief Source was updated by a change in the reported stick position. (outside of the deadzone) + Axis, + + //! \brief Source was updated by a change in the reported stick position. (inside of the deadzone) + AxisDeadzone, + + + //! \brief Source value is greater than provided user value. _(source > param)_ + Greater, + + //! \brief Source value is greater or equal to provided user value. _(source >= param)_ + GreaterOrEqual, + + //! \brief Source value is lower than provided user value. _(source < param)_ + Lower, + + //! \brief Source value is lower or equal to provided user value. _(source <= param)_ + LowerOrEqual, + + //! \brief Source value is equal to provided user value. _(source == param)_ + Equal, + + //! \brief Source value is different than provided user value. _(source != param)_ + NotEqual, + + + //! \brief Checks action if it's enabled and can be activated. + //! \note Does not check if action is currently active. + ActionEnabled, + + //! \brief Checks if action is active via a toggle. + //! \note Check is only valid on toggled actions. + ActionToggleActive, + + //! \brief Checks if action is inactive via a toggle. + //! \note Check is only valid on toggled actions. + ActionToggleInactive, + + //! \brief Always true, can be used to force some steps at the end of an action. + //! \note One use case would be to reset the action's values. + AlwaysTrue, + }; + + enum class InputActionConditionFlags : ice::u8 + { + None = 0, + + //! @brief Performs operation 'series_success |= condition_success' + SeriesOr = None, + + //! @brief Performs operation 'series_success &= condition_success' + SeriesAnd = 0x01, + + //! @brief Checks the series status instead of condition status for 'RunSteps', 'Activate' and 'Final' operations. + SeriesCheck = 0x02, + + //! @brief Finishes series of conditions. + SeriesFinish = 0x04, + + //! @brief Runs all steps attached to a condition. + RunSteps = 0x08, + + // //! @brief Activates an action if the condition/series are successful. Implies 'RunSteps'. + // Activate = 0x10, + + // //! @brief Deactivates an action if the condition/series are successful. Implies 'RunSteps'. + // Deactivate = 0x20, + + //! @brief Stops execution conditions for this action if the series are successful. Implies 'SeriesFinish'. + Final = 0x80 | SeriesFinish, + }; + + //! \brief Steps that can be executed after a condition (or series of conditions) was evaluated successfuly. + enum class InputActionStep : ice::u8 + { + Invalid = 0, + + //! \brief Activates an action for the current frame. + //! \note If used in a 'toggled' action, activation will still only activate it for a single frame. + //! If the action is already 'active' via toggling, then nothing changes. + Activate, + + //! \brief Deactivates an action for all following frames. + //! \note Generally not necessary to be added, since actions are only active for a single frame. + //! If used on a 'toggled' it will deactivate the toggle. + Deactivate, + + //! \brief Used to implement 'toggled' actions, allowing an action to be active even when no conditions are passed. + Toggle, + + //! \brief Resets internal input action values to their defaults. + Reset, + + //! \brief Sets the input actions value to "time passed since activation" (in seconds) + Time, + + //! \brief Sets the input actions value to the value reported by a given source. + Set, + + //! \brief Increases the input actions value by the value reported by a given source. + Add, + + //! \brief Reduces the input actions value by the value reported by a given source. + Sub, + }; + + //! \brief Modifiers that can be applied to action values before publishing. + //! \details Each modifier is re-evaluated every frame as long as the action is active. + //! However because actions use a double-storage approach (raw -> final) the modifiers will not result in infinite + //! divisions (or other actions) of the previous value. + enum class InputActionModifier : ice::u8 + { + Invalid = 0, + + //! \brief Adds param-value from action-value. `action_value - user_param` + Add, + //! \brief Subtracts param-value from action-value. `action_value - user_param` + Sub, + //! \brief Multiplies action-value by param-value. `action_value * user_param` + Mul, + //! \brief Divides action-value by param-value. `action_value / user_param` + //! \note The value cannot be '0'! + Div, + + //! \brief Higher value of the actions value and a user provided param. `max(action_value, user_param)` + MaxOf, + + //! \brief Lower value of the actions value and a user provided param. `min(action_value, user_param)` + MinOf, + }; + + //! \brief Data representation of active input actions. + //! \details When using 'ActionObject' (or 'object' in scripts) the user will receive all available information on that specific + //! action. However because values are stored internally as a 'ice::vec2f' interpretation needs to be done by the consumer. + //! + //! \note This value only affects how data is published to the rest of the engine (using a shard) once an action + //! is active. It does not change the internal storage of actual action data. + enum class InputActionDataType : ice::u8 + { + Invalid = 0, + Bool, + Float1, + Float2, + ActionObject, + + //! \todo Added for possible support of VR inputs in the future. + // Float3, + }; + + //! \brief The check to be performed on an existing action. + //! \note Only used as argument for 'InputActionStack::action_check' + enum class InputActionCheck : ice::u8 + { + None = 0, + Exists = None, + Enabled, + Disabled, + Active, + Inactive, + }; + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action_executor.hxx b/source/code/systems/input_action_system/public/ice/input_action_executor.hxx new file mode 100644 index 00000000..6fd9626e --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action_executor.hxx @@ -0,0 +1,86 @@ +#pragma once +#include + +namespace ice +{ + + //! \brief Helper object to execute conditions, steps and modifiers on action objects and values. + //! \details This class was introduced to properly handle some specific conditions / input sources (ex.: Axis) where + //! we need to check the value against a provided deadzone. Because this value might be set on a subsequent layer, the + //! `InputActionStack` object will prepare the executor before using on each action in each currently activated layer. + //! + //! \note Additional context specific features might be added. One of the current ideas would be custom constants, + //! that could be set by the a user and applied on a different layer. + class InputActionExecutor + { + public: + ~InputActionExecutor() noexcept = default; + + //! \brief Prepares the executor to use constant values defined by the specific layer. + //! \details If a layer does not define constants for themselfs, the previous values will be used. + //! \param layer Layer to load constants from. + void prepare_constants( + ice::InputActionLayer const& layer + ) noexcept; + + //! \brief Checks given condition against the input source and provided user value. + //! \details This method is only called for conditions that are called on input sources. + //! \param[in] condition ID of the condition to be executed. + //! \param[in] input_source The input source that is referenced by this condition. + //! \param[in] param User provided parameter that may be used to complete the check. (Not all conditions will make use this value) + //! \return `true` if the condition was valid and passed the check, `false` otherwise. + bool execute_condition( + ice::InputActionCondition condition, + ice::InputActionSource const& input_source, + ice::f32 param + ) const noexcept; + + //! \brief Checks given condition against an action and provided user value. + //! \details This method is only called for conditions that are called on itself or other input actions. + //! \param[in] condition ID of the condition to be executed. + //! \param[in] action Runtime data of the referenced input action. + //! \param[in] param User provided parameter that may be used to complete the check. (Not all conditions will make use this value) + //! \return `true` if the condition was valid and passed the check, `false` otherwise. + //! + //! \note Input actions are executed in order of definition, so referencing an action that is defined later, + //! might result in undefined behavior. + bool execute_condition( + ice::InputActionCondition condition, + ice::InputActionRuntime const& action, + ice::f32 param + ) const noexcept; + + //! \brief Executes a step over the current actions runtime object, this is generally called for '.activate', '.toggle' and + //! simmilar actions. + //! \param[in] step Action step to be executed. + //! \param[out] runtime Object to be modifed from the action step taken. + void execute_step( + ice::InputActionStep step, + ice::InputActionRuntime& runtime + ) const noexcept; + + //! \brief Executes a step by taking a value from an input source and storing it at a specific input action value. + //! \param[in] step Action step to be executed. + //! \param[in] source_value Input source from which we want to take the new value. + //! \param[out] destination_value Destination value where we want to store the value from the input source. + void execute_step( + ice::InputActionStep step, + ice::InputActionSource const& source_value, + ice::f32& destination_value + ) const noexcept; + + //! \brief Executes an modifier with a user provided parameter over an action value. + //! \param modifier Modifier to be executed on the given value. + //! \param action_value The value to be modified (if necessary). + //! \param param User parameter that may be used by the selected modifier. + void execute_modifier( + ice::InputActionModifier modifier, + ice::f32& action_value, + ice::f32 param + ) const noexcept; + + private: + ice::f32 _constants[Constant_CountInputActionConstants]; + }; + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action_info.hxx b/source/code/systems/input_action_system/public/ice/input_action_info.hxx new file mode 100644 index 00000000..98305caa --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action_info.hxx @@ -0,0 +1,116 @@ +#pragma once +#include + +namespace ice +{ + + //! \brief Predefined list of that can be set from input action scripts. + enum class InputActionConstant : ice::u8 + { + //! \brief Represents the '0.0' value, can't be set from script. + Nil, + + //! \brief Float value used to determine if axis is outside of the deazone. + //! \details The logic will consider values btween `[0, X)` to be inside the deadzone. + ControllerAxisDeadzone, + + //! \brief Represents an unknown / invalid constnat. + Invalid = ice::u8_max, + }; + + static_assert(ice::u32(InputActionConstant::ControllerAxisDeadzone) < Constant_CountInputActionConstants); + + //! \brief Defines a single constant value. + struct InputActionConstantInfo + { + //! \brief Identifies the constant this value is bound to. + ice::InputActionConstant identifier; + + //! \brief Identifiers the offset at which the constant is stored in the binary blob. + ice::u8 offset; + }; + + //! \brief Source types which determine how events are processed to create action values. + //! + //! \note Behavior definitions: + //! `Key` | `Button` => `1.0` on Press, `0.0` on Release. + //! `Axis_d` => `if . gt_or_eq ` then `[deadzone, 1.0]`, else `0.0` + //! \note The `` value is also defined by a input action layer. + enum class InputActionSourceType : ice::u8 + { + Key, + Button, + Trigger, + Axis1d, + Axis2d + }; + + struct InputActionSourceInputInfo + { + //! \brief Offset and size of the name in the data blob. + ice::ref16 name; + + //! \brief Native input ID input source object will get values from. + ice::input::InputID input; + + //! \brief Action Type, which determines how input events are processed. + //! + //! \note Behavior definitions: + //! `Key` | `Button` => `1.0` on Press, `0.0` on Release. + //! `Axis` => `if gt_or_eq ` then `[deadzone, 1.0]`, else `0.0` + ice::InputActionSourceType type; + + //! \brief Layer relative offset where the values from the event should be stored. + ice::u16 storage_offset; + + //! \brief Additional param usable by some source types. + //! \note Only used by 'Axis' sources for deadzone checks. + //! \todo This param is not set or used, however it's here for future work. + ice::f32 param; + }; + + static_assert(sizeof(InputActionSourceInputInfo) % 4 == 0); + + //! \brief Adds additional behavior constraints or features to an action. + enum class InputActionBehavior : ice::u8 + { + //! \brief Only active for as long the activation condition is true. + Default, + + //! \brief Can be toggled to be active even when no 'activation' condition is true. + //! \note A toggled action will still be behave as `Default` one, if the '.toggle' step is not used. + Toggled, + + //! \brief Is active for a single frame after activation. + //! Requires the action to 'deactivate' before a new event will be sent. + ActiveOnce, + }; + + struct InputActionInfo + { + //! \brief Offset and size of the name in the data blob. + ice::ref16 name; + + //! \brief Data representation of the resulting Action. This defines with what type the resulting Shard will be sent. + ice::InputActionDataType type; + + //! \brief Allows to customize behavior for actions. + //! \details With the following: + //! * 'Default' - Action is active as long as it's activation condition is 'true'. + //! * 'Toggled' - Action is active after the activation condition was successful, and deactivated on the next success. + //! * 'Once' - Action is active only for a single frame after activation condition was successful. + //! + //! \note When defining actions in '.ias' scripts, it's better to use '.toggle' step instead of '.activate' + //! for additional readability, however the '.toggle' step is only allowed in 'toggled' input actions. + ice::InputActionBehavior behavior; + + //! \brief Offset and count of conditions this action has defined. + ice::ref16 conditions; + + //! \brief Offset and count of modifiers this action has defined. + ice::ref8 mods; + }; + + static_assert(sizeof(InputActionInfo) % 4 == 0); + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action_layer.hxx b/source/code/systems/input_action_system/public/ice/input_action_layer.hxx new file mode 100644 index 00000000..325e88b3 --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action_layer.hxx @@ -0,0 +1,106 @@ +#pragma once +#include +#include +#include +#include + +namespace ice +{ + + //! \brief An action layer represents a set of actions, grouped by some logic the developer. + //! \details Each layer may define any number of sources (or none) and any number of actions (or none). + //! When evaluated during runtime using an action stack, it will overshadow input source events on layers below the top one, + //! which in turn allows to have multiple input contexts depending on the game state. Pushing or popping a layer can be done + //! at the end of each frame to prepare input processing for the next frame. + //! + //! \note The layer contains only the definitions of it's sources and actions. It does not contain runtime data + //! that is used. The runtime values are owned and handled by InputActionStack object, which is also responsible to + //! properly resolve references to the same actions or sources between layers. + class InputActionLayer + { + public: + virtual ~InputActionLayer() noexcept = default; + + //! \return Name of this action layer. + virtual auto name() const noexcept -> ice::String = 0; + + //! \return List of all sources defined by this layer. + virtual auto sources() const noexcept -> ice::Span = 0; + + //! \return Fetches the name of the given source, based on it's definition. + virtual auto source_name(ice::InputActionSourceInputInfo const& source) const noexcept -> ice::String = 0; + + //! \return List of all actions defined by this layer. + virtual auto actions() const noexcept -> ice::Span = 0; + + //! \return Fetches the name of the given action, based on it's definition. + virtual auto action_name(ice::InputActionInfo const& action) const noexcept -> ice::String = 0; + + //! \brief Loads all defined constants into the given span. + //! \note The span needs to hold at least `ice::Constant_CountInputActionConstants` number of entries. + //! \param[in,out] constants_span The span into which individual constant values will be loaded. + //! \returns Number of loaded constants or `0` if no constants are defined in this layer. + virtual auto load_constants(ice::Span constants_span) const noexcept -> ice::ucount = 0; + + //! \brief Updates all layer sources based on the input events passed. + //! \param[in,out] events List of input events to be processed. If an event was processed it will be swapped with an + //! event at the end of the list and reset to `ice::input::InputEvent{}`. + //! \param[in,out] source_values List of storage objects for each defined input source. + //! \return Number of processed event's that have been consumed and reset. + virtual auto process_inputs( + ice::Span events, + ice::Span source_values + ) const noexcept -> ice::ucount = 0; + + //! \brief Runs updates on all defined actions by this layer. + //! \param[in] executor Executor object with context data, used to execute conditions, steps and modifiers. + //! \param[in] source_values List of storage objects for each defined input source. + //! \param[in,out] actions List of runtime objects for each action defined by this layer. + //! \return `true` + //! + //! \todo Might need to rethink the return value, currently only returning true. + virtual bool update_actions( + ice::InputActionExecutor const& executor, + ice::Span source_values, + ice::HashMap& actions + ) const noexcept = 0; + }; + + //! \brief Creates input action layer from binary data. + //! \param alloc Allocator used to create the object and any other required objects. + //! \param layer_data Binary definitions of an action layer. + //! \return `InputActionLayer` object or `nullptr` if failed to parse input data. + //! + //! \todo As of now, the function may crash if parsing fails. + auto create_input_action_layer( + ice::Allocator& alloc, + ice::Data layer_data + ) noexcept -> ice::UniquePtr; + + //! \copydoc ice::create_input_action_layer(ice::Allocator&,ice::Memory) + auto create_input_action_layer( + ice::Allocator& alloc, + ice::Memory layer_data + ) noexcept -> ice::UniquePtr; + + //! \brief Creates input action layer from script. + //! \param alloc Allocator used to create the object and any other required objects. + //! \param definition Script definitions of an input action layer. + //! \return `InputActionLayer` object or `nullptr` if failed to parse input data. + //! + //! \todo As of now, the function may crash if parsing fails. + auto parse_input_action_layer( + ice::Allocator& alloc, + ice::String definition + ) noexcept -> ice::Array>; + + //! \brief Creates a binary representation of an InputActionLayer object that can be loaded again later. + //! \param alloc Allocator used to allocate the final Memory object. + //! \param action_layer Layer to be saved in binary form. + //! \return Allocated Memory object if operation was successful, empty if no data was saved. + auto save_input_action_layer( + ice::Allocator& alloc, + ice::InputActionLayer const& action_layer + ) noexcept -> ice::Expected; + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action_layer_builder.hxx b/source/code/systems/input_action_system/public/ice/input_action_layer_builder.hxx new file mode 100644 index 00000000..d478e2ee --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action_layer_builder.hxx @@ -0,0 +1,123 @@ +#pragma once +#include +#include +#include + +namespace ice +{ + + struct InputActionBuilder + { + //! \brief Builder for input action layer objects. Allows to define Sources and Actions. + class Layer; + + //! \brief Builder for input action sources. Allows to define a source and associated input events. + class Source; + + //! \brief Builder for input actions. Allows to define an action, along with it's conditions and modifiers. + class Action; + + //! \brief Builder for input action condition series. Defines multiple conditions with steps to run when + //! evaluated as 'true'. + class ConditionSeries; + + //! \brief Utility base class for Builder types. + using BuilderBase = ice::concepts::PimplType; + }; + + //! \brief Builder object for input action layers. Allows to easily define actions and sources without + //! the need to understand the underlying binary representation. + class InputActionBuilder::Layer : public BuilderBase + { + public: + using BuilderBase::BuilderBase; + + virtual ~Layer() = default; + + virtual auto set_name( + ice::String name + ) noexcept -> ice::InputActionBuilder::Layer& = 0; + + virtual void set_constant( + ice::InputActionConstant constant, + ice::f32 value + ) noexcept = 0; + + virtual auto define_source( + ice::String name, + ice::InputActionSourceType type + ) noexcept -> ice::InputActionBuilder::Source = 0; + + virtual auto define_action( + ice::String name, + ice::InputActionDataType type + ) noexcept -> ice::InputActionBuilder::Action = 0; + + virtual auto finalize( + ice::Allocator& alloc + ) noexcept -> ice::UniquePtr = 0; + }; + + class InputActionBuilder::Source : public BuilderBase + { + public: + using BuilderBase::BuilderBase; + + auto add_key(ice::input::KeyboardKey key) noexcept -> Source&; + auto add_keymod(ice::input::KeyboardMod keymod) noexcept -> Source&; + auto add_button(ice::input::MouseInput button) noexcept -> Source&; + auto add_button(ice::input::ControllerInput button) noexcept -> Source&; + auto add_axis(ice::input::MouseInput axisx) noexcept -> Source&; + auto add_axis(ice::input::ControllerInput button) noexcept -> Source&; + }; + + class InputActionBuilder::ConditionSeries : public BuilderBase + { + public: + using BuilderBase::BuilderBase; + + void set_finished(bool can_finalize_condition_checks = true) noexcept; + + auto add_condition( + ice::String source, + ice::InputActionCondition condition, + ice::InputActionConditionFlags flags = InputActionConditionFlags::None, + ice::f32 param = 0.0f, + bool from_action = false + ) noexcept -> ConditionSeries&; + + auto add_step( + ice::InputActionStep step + ) noexcept -> ConditionSeries&; + + auto add_step( + ice::String source, + ice::InputActionStep step, + ice::String target_axis = ".x" + ) noexcept -> ConditionSeries&; + }; + + class InputActionBuilder::Action : public BuilderBase + { + public: + using BuilderBase::BuilderBase; + + auto add_condition_series() noexcept -> ConditionSeries; + + auto set_behavior( + ice::InputActionBehavior behavior + ) noexcept -> Action&; + + auto add_modifier( + ice::InputActionModifier modifier, + ice::f32 param, + ice::String target_axis = ".x" + ) noexcept -> Action&; + }; + + auto create_input_action_layer_builder( + ice::Allocator& alloc, + ice::String layer_name + ) noexcept -> ice::UniquePtr; + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action_stack.hxx b/source/code/systems/input_action_system/public/ice/input_action_stack.hxx new file mode 100644 index 00000000..3ae16770 --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action_stack.hxx @@ -0,0 +1,96 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace ice +{ + + static constexpr ice::ErrorCode S_LayerAlreadyRegistered{ "S.2300:InputAction:The passed input action layer is already registered."}; + static constexpr ice::ErrorCode E_UnknownInputAction{ "E.2300:InputAction:The requested input action does not exist." }; + static constexpr ice::ErrorCode E_InputActionDisabled{ "E.2301:InputAction:The requested input action is disabled and cannot be accessed." }; + static constexpr ice::ErrorCode E_InputActionInactive{ "E.2302:InputAction:The requested input action is inactive and cannot be accessed." }; + + //! \brief Represents an stack of currently registered input layers. + //! \details Layers can be stacked on each other with a priority value, which starts at '0' and grows. + //! If the stack contains a layer, which priority is lower than a layer processed previously, that layer will not be processed. + class InputActionStack + { + public: + virtual ~InputActionStack() noexcept = default; + + virtual auto registered_layers( + ice::Array& out_layers + ) const noexcept -> ice::ucount = 0; + + virtual auto register_layer( + ice::InputActionLayer const* layer + ) noexcept -> ice::Result = 0; + + + virtual auto active_layers( + ice::Array& out_layers + ) const noexcept -> ice::ucount = 0; + + virtual void push_layer( + ice::InputActionLayer const* layer + ) noexcept = 0; + + virtual void pop_layer( + ice::InputActionLayer const* layer + ) noexcept = 0; + + + virtual auto action( + ice::String action + ) const noexcept -> ice::Expected = 0; + + virtual bool action_check( + ice::String action, + ice::InputActionCheck check = InputActionCheck::Active + ) const noexcept = 0; + + virtual auto action_time( + ice::String action + ) const noexcept -> ice::Tms = 0; + + virtual bool action_value( + ice::String action, + ice::vec2f& out_value + ) const noexcept = 0; + + virtual bool action_value( + ice::String action, + ice::vec2f& out_value, + ice::Tns& out_timeactive + ) const noexcept = 0; + + + virtual void process_inputs( + ice::Span events + ) noexcept = 0; + + virtual auto publish_shards( + ice::ShardContainer& out_shards + ) const noexcept -> void = 0; + + + virtual auto action_runtime( + ice::InputActionLayer const& layer, + ice::InputActionInfo const& action_info + ) const noexcept -> ice::InputActionRuntime const& = 0; + + virtual auto source_runtime( + ice::InputActionLayer const& layer, + ice::InputActionSourceInputInfo const& source_info + ) const noexcept -> ice::InputActionSource const& = 0; + }; + + auto create_input_action_stack( + ice::Allocator& alloc, + ice::String actionid_prefix + ) noexcept -> ice::UniquePtr; + +} // namespace ice diff --git a/source/code/systems/input_action_system/public/ice/input_action_types.hxx b/source/code/systems/input_action_system/public/ice/input_action_types.hxx new file mode 100644 index 00000000..65b6cbda --- /dev/null +++ b/source/code/systems/input_action_system/public/ice/input_action_types.hxx @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace ice +{ + + enum class InputActionBehavior : ice::u8; + enum class InputActionCheck : ice::u8; + enum class InputActionCondition : ice::u8; + enum class InputActionConstant : ice::u8; + enum class InputActionDataType : ice::u8; + enum class InputActionModifier : ice::u8; + enum class InputActionSourceEvent : ice::u8; + enum class InputActionSourceType : ice::u8; + enum class InputActionStep : ice::u8; + + struct InputAction; + struct InputActionConstantInfo; + struct InputActionInfo; + struct InputActionRuntime; + struct InputActionSource; + struct InputActionSourceInputInfo; + + struct InputActionConditionData; + struct InputActionModifierData; + struct InputActionStepData; + + class InputActionExecutor; + class InputActionLayer; + class InputActionStack; + + static constexpr ice::u32 Constant_CountInputActionConstants = 2; + +} // namespace ice + +template<> +constexpr inline ice::ShardPayloadID ice::Constant_ShardPayloadID = ice::shard_payloadid("ice::InputAction const*"); diff --git a/source/code/systems/input_system/private/input_mouse.cxx b/source/code/systems/input_system/private/input_mouse.cxx index a1e6a73f..d410cc60 100644 --- a/source/code/systems/input_system/private/input_mouse.cxx +++ b/source/code/systems/input_system/private/input_mouse.cxx @@ -147,6 +147,8 @@ namespace ice::input ice::array::push_back(events_out, event); } + // All other events have an axis_idx == 0 + event.axis_idx = 0; if (_wheel != 0) { event.identifier = input_identifier(DeviceType::Mouse, MouseInput::Wheel); diff --git a/source/code/systems/input_system/public/ice/input/input_keyboard.hxx b/source/code/systems/input_system/public/ice/input/input_keyboard.hxx index ef1fdbab..09269e8d 100644 --- a/source/code/systems/input_system/public/ice/input/input_keyboard.hxx +++ b/source/code/systems/input_system/public/ice/input/input_keyboard.hxx @@ -140,6 +140,9 @@ namespace ice::input KeyRightAlt, KeyRightGui, + KeyMode, + KeyCapsLock, + NumPadNumlockClear, NumPadDivide, NumPadMultiply, diff --git a/source/code/systems/resource_system/public/ice/resource_compiler_api.hxx b/source/code/systems/resource_system/public/ice/resource_compiler_api.hxx index 3a7f2020..ca177533 100644 --- a/source/code/systems/resource_system/public/ice/resource_compiler_api.hxx +++ b/source/code/systems/resource_system/public/ice/resource_compiler_api.hxx @@ -26,7 +26,7 @@ namespace ice using FnSupportedResources = auto(*)( ice::Span params - ) noexcept -> ice::Span; + ) noexcept -> ice::Span; using FnPrepareContext = auto(*)( ice::Allocator& alloc, @@ -140,6 +140,7 @@ namespace ice static constexpr ice::StringID Constant_APIName = "ice.api.resource-compiler.v1"_sid; static constexpr ice::u32 Constant_APIVersion = 1; + StringID id_category; FnSupportedResources fn_supported_resources = nullptr; FnPrepareContext fn_prepare_context = nullptr; FnCleanupContext fn_cleanup_context = nullptr; diff --git a/source/code/test/private/game.cxx b/source/code/test/private/game.cxx index 78c41adf..4aafa95d 100644 --- a/source/code/test/private/game.cxx +++ b/source/code/test/private/game.cxx @@ -2,6 +2,7 @@ /// SPDX-License-Identifier: MIT #include "game.hxx" +#include "input_actions.hxx" #include @@ -224,11 +225,16 @@ auto test_factory(ice::Allocator& alloc, ice::TraitContext& context, void*) noex { return ice::make_unique(alloc, alloc, context); } +auto actions_factory(ice::Allocator& alloc, ice::TraitContext& context, void*) noexcept -> UniquePtr +{ + return context.make_unique(alloc, alloc); +} bool test_reg_traits(ice::TraitArchive& arch) noexcept { arch.register_trait({ .name = "act"_sid, .fn_factory = act_factory }); arch.register_trait({ .name = "test"_sid, .fn_factory = test_factory }); + arch.register_trait({ .name = "actions"_sid, .fn_factory = actions_factory }); return true; } @@ -292,6 +298,7 @@ void TestGame::on_resume(ice::Engine& engine) noexcept ice::StringID traits[]{ "act"_sid, "test2"_sid, + "actions"_sid, ice::devui_trait_name(), ice::TraitID_GfxShaderStorage }; diff --git a/source/code/test/private/input_actions.cxx b/source/code/test/private/input_actions.cxx new file mode 100644 index 00000000..13e3c3c5 --- /dev/null +++ b/source/code/test/private/input_actions.cxx @@ -0,0 +1,133 @@ +#include "input_actions.hxx" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ice +{ + + // static constexpr ice::ShardID ShardID_ClickAction = "ice/input-action/A:click`ice::InputAction const*"_shardid; + + InputActionsTrait::InputActionsTrait(ice::TraitContext& context, ice::Allocator& alloc) noexcept + : ice::Trait{ context } + , ice::TraitDevUI{ {.category="Engine/Traits", .name = trait_name()} } + , _allocator{ alloc, "trait :: input-actions" } + , _layers{ _allocator } + { + context.bind<&InputActionsTrait::on_update>(); + context.bind<&InputActionsTrait::on_click>("Click`ice::InputAction const*"_shardid); + } + + auto InputActionsTrait::on_click(ice::InputAction const& action) noexcept -> ice::Task<> + { + ice::Tns const timeactive = ice::clock::elapsed(action.timestamp, ice::clock::now()); + ICE_LOG(LogSeverity::Info, LogTag::Engine, "{:.2s} Click | {}x{}", timeactive, action.value.x, action.value.y); + + co_return; + } + + auto InputActionsTrait::activate( + ice::WorldStateParams const& world_update + ) noexcept -> ice::Task<> + { + using namespace ice::input; + using enum ice::InputActionConditionFlags; + _stack = ice::addressof(world_update.engine.actions()); + + ice::AssetStorage& assets = world_update.assets; + ice::Asset script = assets.bind(ice::AssetCategory_InputActionsScript, "core/example_input_actions"); + if (script.valid() == false) + { + co_return; + } + + ice::Data const data = co_await script[AssetState::Raw]; + if (data.location != nullptr) + { + _layers = ice::parse_input_action_layer(_allocator, ice::string::from_data(data)); + } + + + + for (ice::UniquePtr const& layer : _layers) + { + _stack->register_layer(layer.get()); + _stack->push_layer(layer.get()); + } + co_return; + } + + auto InputActionsTrait::on_update(ice::EngineFrameUpdate const& update) noexcept -> ice::Task<> + { + co_return; + } + + auto InputActionsTrait::deactivate(ice::WorldStateParams const& world_update) noexcept -> ice::Task<> + { + co_return; + } + + auto evname(InputActionSourceEvent ev) -> char const* + { + switch(ev) + { + using enum InputActionSourceEvent; + case None: return "-"; + case Trigger: return "trigger"; + case ButtonPress: return "pressed"; + case ButtonRelease: return "released"; + case Axis: return "axis"; + case AxisDeadzone: return "deadzone"; + default: return "?"; + } + } + + void InputActionsTrait::build_content() noexcept + { + for (ice::UniquePtr const& layer : _layers) + { + ImGui::Separator(); + ImGui::TextT("{}", layer->name()); + ImGui::SeparatorText("Sources"); + + ice::u32 last_storage = ice::u32_max; + for (ice::InputActionSourceInputInfo const& source_info : layer->sources()) + { + ice::InputActionSource const& source = _stack->source_runtime(*layer, source_info); + if (ice::exchange(last_storage, source_info.storage_offset) == source_info.storage_offset) + { + continue; + } + + if (source_info.type == InputActionSourceType::Axis2d) + { + ImGui::TextT("Source: {}, value:{}x{}", layer->source_name(source_info), source.value, (&source + 1)->value); + } + else + { + ImGui::TextT("Source: {}, value:{} ({})", layer->source_name(source_info), source.value, evname(source.event)); + } + } + ImGui::SeparatorText("Actions"); + for (ice::InputActionInfo const& action_info : layer->actions()) + { + ice::InputActionRuntime const& action = _stack->action_runtime(*layer, action_info); + ImGui::TextT("Action: {}, active:{}, value:{},{}", layer->action_name(action_info), action.active, action.value.x, action.value.y); + } + } + } + +} // namespace ice diff --git a/source/code/test/private/input_actions.hxx b/source/code/test/private/input_actions.hxx new file mode 100644 index 00000000..55fafe95 --- /dev/null +++ b/source/code/test/private/input_actions.hxx @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include + +namespace ice +{ + + class InputActionsTrait final + : public ice::Trait + , public ice::TraitDevUI + , public ice::InterfaceSelectorOf + { + public: + InputActionsTrait(ice::TraitContext& context, ice::Allocator& alloc) noexcept; + + auto activate(ice::WorldStateParams const& world_update) noexcept -> ice::Task<> override; + auto deactivate(ice::WorldStateParams const& world_update) noexcept -> ice::Task<> override; + + auto on_click(ice::InputAction const& action) noexcept -> ice::Task<>; + auto on_update(ice::EngineFrameUpdate const& update) noexcept -> ice::Task<>; + + public: // TraitDevUI + auto trait_name() const noexcept -> ice::String override { return "Engine.InputActions"; } + void build_content() noexcept override; + + private: + ice::ProxyAllocator _allocator; + ice::InputActionStack* _stack; + ice::Array> _layers; + }; + +} // namespace ice diff --git a/source/configs.bff b/source/configs.bff index 78b545cd..02d8a31e 100644 --- a/source/configs.bff +++ b/source/configs.bff @@ -66,6 +66,7 @@ '-Werror' // We use [[deprecated]] to keep track of some old API usage without the need to fix them asap. '-Wno-error=deprecated-declarations' + '-Wno-#pragma-messages' // Currently added to not fail on small issues '-Wno-unused-private-field' '-Wno-unused-const-variable' diff --git a/source/data/core/example_input_actions.ias b/source/data/core/example_input_actions.ias new file mode 100644 index 00000000..fcb67270 --- /dev/null +++ b/source/data/core/example_input_actions.ias @@ -0,0 +1,60 @@ +layer Base: + source button RClick: mouse.rbutton + source axis2d Pos: mouse.pos + + action RClick: float2 + when RClick.pressed + .x = Pos.x + .y = Pos.y + .activate + +layer Test: + source button Jump: kb.Space + + source button Left: kb.A, kb.Left + source button Right: kb.D, kb.Right + source button Up: kb.W, kb.Up + source button Down: kb.S, kb.Down + + source axis2d Pos + source button Click: mouse.lbutton + + source button Sprint: kb.lshift + source button SprintToggle: kb.V + + action Sprint: bool, toggled + when SprintToggle.pressed + .toggle + when Sprint.pressed + .activate + when Sprint.released + .deactivate + + action Jump: float1 + when Jump.pressed + .time + .activate + mod .x min 1.0 + + action Move: float2 + when Left.pressed + .x - Left.x + or Right.pressed + .x + Right.x + or Up.pressed + .y + Up.x + or Down.pressed, series + .y - Down.x + .activate + + when .true // only executed if the previous condition series are not valid + .reset + + action Click: object, once + when Click.pressed + .x = Pos.x + .y = Pos.y + .activate + +layer Constants: + constant axis.deadzone = 0.25 diff --git a/tools/conanfile.txt b/tools/conanfile.txt index e77f6820..10335a1e 100644 --- a/tools/conanfile.txt +++ b/tools/conanfile.txt @@ -1,6 +1,6 @@ [requires] fastbuild-installer/1.12@iceshard/stable -ice-build-tools/1.11.0@iceshard/stable +ice-build-tools/1.11.1@iceshard/stable [generators] VirtualRunEnv