diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index a44cac2cd1a..877273d7645 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -35,14 +35,14 @@ jobs: - name: Install Dependencies run: | - cd "Tools/changelog" + cd "Tools/changelogs" npm install shell: bash continue-on-error: true - name: Generate Changelog run: | - cd "Tools/changelog" + cd "Tools/changelogs" node changelog.js shell: bash continue-on-error: true diff --git a/.github/workflows/prtitlecase.yml b/.github/workflows/prtitlecase.yml new file mode 100644 index 00000000000..b3150dcc7e9 --- /dev/null +++ b/.github/workflows/prtitlecase.yml @@ -0,0 +1,34 @@ +name: PR Title Case +on: + pull_request_target: + types: [opened, edited, synchronize] + +env: + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + +jobs: + prtitlecase: + runs-on: ubuntu-latest + steps: + - name: Checkout Master + uses: actions/checkout@v3 + with: + token: ${{ secrets.BOT_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Install Dependencies + run: | + cd "Tools/prtitlecase" + npm install + shell: bash + + - name: Change Title + run: | + cd "Tools/prtitlecase" + node index.js + shell: bash diff --git a/BuildChecker/BuildChecker.csproj b/BuildChecker/BuildChecker.csproj index 63d16fa9708..d4f9a412549 100644 --- a/BuildChecker/BuildChecker.csproj +++ b/BuildChecker/BuildChecker.csproj @@ -14,8 +14,6 @@ https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild --> - python3 - py -3 {C899FCA4-7037-4E49-ABC2-44DE72487110} .NETFramework, Version=v4.7.2 false @@ -39,7 +37,7 @@ https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild bin\DebugOpt\ - + diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py deleted file mode 100644 index becd4506e82..00000000000 --- a/BuildChecker/git_helper.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 -# Installs git hooks, updates them, updates submodules, that kind of thing. - -import subprocess -import sys -import os -import shutil -from pathlib import Path -from typing import List - -SOLUTION_PATH = Path("..") / "SpaceStation14.sln" -# If this doesn't match the saved version we overwrite them all. -CURRENT_HOOKS_VERSION = "2" -QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet" - - -def run_command(command: List[str], capture: bool = False) -> subprocess.CompletedProcess: - """ - Runs a command with pretty output. - """ - text = ' '.join(command) - if not QUIET: - print("$ {}".format(text)) - - sys.stdout.flush() - - completed = None - - if capture: - completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE) - else: - completed = subprocess.run(command, cwd="..") - - if completed.returncode != 0: - print("Error: command exited with code {}!".format(completed.returncode)) - - return completed - - -def update_submodules(): - """ - Updates all submodules. - """ - - if ('GITHUB_ACTIONS' in os.environ): - return - - if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"): - return - - if shutil.which("git") is None: - raise FileNotFoundError("git not found in PATH") - - # If the status doesn't match, force VS to reload the solution. - # status = run_command(["git", "submodule", "status"], capture=True) - run_command(["git", "submodule", "update", "--init", "--recursive"]) - # status2 = run_command(["git", "submodule", "status"], capture=True) - - # Something changed. - # if status.stdout != status2.stdout: - # print("Git submodules changed. Reloading solution.") - # reset_solution() - - -def install_hooks(): - """ - Installs the necessary git hooks into .git/hooks. - """ - - # Read version file. - if os.path.isfile("INSTALLED_HOOKS_VERSION"): - with open("INSTALLED_HOOKS_VERSION", "r") as f: - if f.read() == CURRENT_HOOKS_VERSION: - if not QUIET: - print("No hooks change detected.") - return - - with open("INSTALLED_HOOKS_VERSION", "w") as f: - f.write(CURRENT_HOOKS_VERSION) - - print("Hooks need updating.") - - hooks_target_dir = Path("..")/".git"/"hooks" - hooks_source_dir = Path("hooks") - - # Clear entire tree since we need to kill deleted files too. - for filename in os.listdir(str(hooks_target_dir)): - os.remove(str(hooks_target_dir/filename)) - - for filename in os.listdir(str(hooks_source_dir)): - print("Copying hook {}".format(filename)) - shutil.copy2(str(hooks_source_dir/filename), - str(hooks_target_dir/filename)) - - -def reset_solution(): - """ - Force VS to think the solution has been changed to prompt the user to reload it, thus fixing any load errors. - """ - - with SOLUTION_PATH.open("r") as f: - content = f.read() - - with SOLUTION_PATH.open("w") as f: - f.write(content) - - -if __name__ == '__main__': - install_hooks() - update_submodules() diff --git a/BuildChecker/hooks/post-checkout b/BuildChecker/hooks/post-checkout deleted file mode 100755 index c5662445c27..00000000000 --- a/BuildChecker/hooks/post-checkout +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -gitroot=`git rev-parse --show-toplevel` - -cd "$gitroot/BuildChecker" - -if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then - # Windows - py -3 git_helper.py --quiet -else - # Not Windows, so probably some other Unix thing. - python3 git_helper.py --quiet -fi diff --git a/BuildChecker/hooks/post-merge b/BuildChecker/hooks/post-merge deleted file mode 100755 index 85fe61d966c..00000000000 --- a/BuildChecker/hooks/post-merge +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# Just call post-checkout since it does the same thing. -gitroot=`git rev-parse --show-toplevel` -bash "$gitroot/.git/hooks/post-checkout" diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs index ea5aa3cf256..2dbe923b2a6 100644 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs @@ -18,15 +18,6 @@ public override Control GetUIFragmentRoot() public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) { _fragment = new CrimeAssistUiFragment(); - - _fragment.OnSync += _ => SendSyncMessage(userInterface); - } - - private void SendSyncMessage(BoundUserInterface userInterface) - { - var syncMessage = new CrimeAssistSyncMessageEvent(); - var message = new CartridgeUiMessage(syncMessage); - userInterface.SendMessage(message); } public override void UpdateState(BoundUserInterfaceState state) diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs index e3163975d12..fb085a8a799 100644 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs @@ -1,7 +1,6 @@ using Content.Client.Message; using Content.Shared.DeltaV.CartridgeLoader.Cartridges; using Robust.Client.AutoGenerated; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; @@ -13,9 +12,7 @@ namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; public sealed partial class CrimeAssistUiFragment : BoxContainer { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IResourceCache _resourceCache = default!; - public event Action? OnSync; private CrimeAssistPage _currentPage; private List? _pages; diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml new file mode 100644 index 00000000000..2de8a37ff77 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs new file mode 100644 index 00000000000..e8dd4eea446 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs @@ -0,0 +1,21 @@ +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class SecWatchEntryControl : BoxContainer +{ + public SecWatchEntryControl(SecWatchEntry entry) + { + RobustXamlLoader.Load(this); + + Status.Text = Loc.GetString($"criminal-records-status-{entry.Status.ToString().ToLower()}"); + Title.Text = Loc.GetString("sec-watch-entry", ("name", entry.Name), ("job", entry.Job)); + + Reason.Text = entry.Reason ?? Loc.GetString("sec-watch-no-reason"); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs new file mode 100644 index 00000000000..da5ff825b91 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs @@ -0,0 +1,27 @@ +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class SecWatchUi : UIFragment +{ + private SecWatchUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface ui, EntityUid? owner) + { + _fragment = new SecWatchUiFragment(); + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is SecWatchUiState cast) + _fragment?.UpdateState(cast); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml new file mode 100644 index 00000000000..7fb2c42debc --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml @@ -0,0 +1,13 @@ + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs new file mode 100644 index 00000000000..ad152840529 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs @@ -0,0 +1,25 @@ +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class SecWatchUiFragment : BoxContainer +{ + public SecWatchUiFragment() + { + RobustXamlLoader.Load(this); + } + + public void UpdateState(SecWatchUiState state) + { + NoEntries.Visible = state.Entries.Count == 0; + Entries.RemoveAllChildren(); + foreach (var entry in state.Entries) + { + Entries.AddChild(new SecWatchEntryControl(entry)); + } + } +} diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 53a8051eecf..a1fc68bbd2f 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -124,6 +124,7 @@ public override void Init() _prototypeManager.RegisterIgnore("wireLayout"); _prototypeManager.RegisterIgnore("alertLevels"); _prototypeManager.RegisterIgnore("nukeopsRole"); + _prototypeManager.RegisterIgnore("stationGoal"); _componentFactory.GenerateNetIds(); _adminManager.Initialize(); diff --git a/Content.Client/Gameplay/GameplayState.cs b/Content.Client/Gameplay/GameplayState.cs index 1efee978f39..2ea16521e8f 100644 --- a/Content.Client/Gameplay/GameplayState.cs +++ b/Content.Client/Gameplay/GameplayState.cs @@ -93,17 +93,17 @@ private void LoadMainScreen() var screenTypeString = _configurationManager.GetCVar(CCVars.UILayout); if (!Enum.TryParse(screenTypeString, out ScreenType screenType)) { - screenType = default; + screenType = ScreenType.Separated; } switch (screenType) { - case ScreenType.Default: - _uiManager.LoadScreen(); - break; case ScreenType.Separated: _uiManager.LoadScreen(); break; + case ScreenType.Overlay: + _uiManager.LoadScreen(); + break; } _loadController.LoadScreen(); diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 5bae35da5ba..8087d1833e6 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; @@ -30,6 +31,15 @@ private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent UpdateLayers(component, sprite); ApplyMarkingSet(component, sprite); + var speciesPrototype = _prototypeManager.Index(component.Species); + + var height = Math.Clamp(component.Height, speciesPrototype.MinHeight, speciesPrototype.MaxHeight); + var width = Math.Clamp(component.Width, speciesPrototype.MinWidth, speciesPrototype.MaxWidth); + component.Height = height; + component.Width = width; + + sprite.Scale = new Vector2(width, height); + sprite[sprite.LayerMapReserveBlank(HumanoidVisualLayers.Eyes)].Color = component.EyeColor; } @@ -194,6 +204,8 @@ public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile humanoid.Species = profile.Species; humanoid.SkinColor = profile.Appearance.SkinColor; humanoid.EyeColor = profile.Appearance.EyeColor; + humanoid.Height = profile.Height; + humanoid.Width = profile.Width; UpdateSprite(humanoid, Comp(uid)); } diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 03f4f3f38b7..fa631938100 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -55,6 +55,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.UseItemInHand); human.AddFunction(ContentKeyFunctions.AltUseItemInHand); human.AddFunction(ContentKeyFunctions.OpenCharacterMenu); + human.AddFunction(ContentKeyFunctions.OpenLanguageMenu); human.AddFunction(ContentKeyFunctions.ActivateItemInWorld); human.AddFunction(ContentKeyFunctions.ThrowItemInHand); human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld); diff --git a/Content.Client/Language/LanguageMenuWindow.xaml b/Content.Client/Language/LanguageMenuWindow.xaml new file mode 100644 index 00000000000..ff33a6ddf56 --- /dev/null +++ b/Content.Client/Language/LanguageMenuWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs new file mode 100644 index 00000000000..312814aca35 --- /dev/null +++ b/Content.Client/Language/LanguageMenuWindow.xaml.cs @@ -0,0 +1,134 @@ +using Content.Client.Language.Systems; +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Console; +using Robust.Shared.Utility; +using Serilog; +using static Content.Shared.Language.Systems.SharedLanguageSystem; + +namespace Content.Client.Language; + +[GenerateTypedNameReferences] +public sealed partial class LanguageMenuWindow : DefaultWindow +{ + private readonly LanguageSystem _clientLanguageSystem; + private readonly List _entries = new(); + + + public LanguageMenuWindow() + { + RobustXamlLoader.Load(this); + _clientLanguageSystem = IoCManager.Resolve().GetEntitySystem(); + } + + protected override void Opened() + { + // Refresh the window when it gets opened. + // This actually causes two refreshes: one immediately, and one after the server sends a state message. + UpdateState(_clientLanguageSystem.CurrentLanguage, _clientLanguageSystem.SpokenLanguages); + _clientLanguageSystem.RequestStateUpdate(); + } + + + public void UpdateState(string currentLanguage, List spokenLanguages) + { + var langName = Loc.GetString($"language-{currentLanguage}-name"); + CurrentLanguageLabel.Text = Loc.GetString("language-menu-current-language", ("language", langName)); + + OptionsList.RemoveAllChildren(); + _entries.Clear(); + + foreach (var language in spokenLanguages) + { + AddLanguageEntry(language); + } + + // Disable the button for the currently chosen language + foreach (var entry in _entries) + { + if (entry.button != null) + entry.button.Disabled = entry.language == currentLanguage; + } + } + + private void AddLanguageEntry(string language) + { + var proto = _clientLanguageSystem.GetLanguagePrototype(language); + var state = new EntryState { language = language }; + + var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Vertical }; + + #region Header + var header = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + HorizontalExpand = true, + SeparationOverride = 2 + }; + + var name = new Label + { + Text = proto?.Name ?? Loc.GetString("generic-error"), + MinWidth = 50, + HorizontalExpand = true + }; + + var button = new Button { Text = "Choose" }; + button.OnPressed += _ => OnLanguageChosen(language); + state.button = button; + + header.AddChild(name); + header.AddChild(button); + + container.AddChild(header); + #endregion + + #region Collapsible description + var body = new CollapsibleBody + { + HorizontalExpand = true, + Margin = new Thickness(4f, 4f) + }; + + var description = new RichTextLabel { HorizontalExpand = true }; + description.SetMessage(proto?.Description ?? Loc.GetString("generic-error")); + body.AddChild(description); + + var collapser = new Collapsible(Loc.GetString("language-menu-description-header"), body) + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + HorizontalExpand = true + }; + + container.AddChild(collapser); + #endregion + + // Before adding, wrap the new container in a PanelContainer to give it a distinct look + var wrapper = new PanelContainer(); + wrapper.StyleClasses.Add("PdaBorderRect"); + + wrapper.AddChild(container); + OptionsList.AddChild(wrapper); + + _entries.Add(state); + } + + + private void OnLanguageChosen(string id) + { + var proto = _clientLanguageSystem.GetLanguagePrototype(id); + if (proto != null) + _clientLanguageSystem.RequestSetLanguage(proto); + } + + + private struct EntryState + { + public string language; + public Button? button; + } +} diff --git a/Content.Client/Language/Systems/LanguageSystem.cs b/Content.Client/Language/Systems/LanguageSystem.cs new file mode 100644 index 00000000000..9714078b2c5 --- /dev/null +++ b/Content.Client/Language/Systems/LanguageSystem.cs @@ -0,0 +1,76 @@ +using Content.Shared.Language; +using Content.Shared.Language.Events; +using Content.Shared.Language.Systems; +using Robust.Client; +using Robust.Shared.Console; + +namespace Content.Client.Language.Systems; + +/// +/// Client-side language system. +/// +/// +/// Unlike the server, the client is not aware of other entities' languages; it's only notified about the entity that it posesses. +/// Due to that, this system stores such information in a static manner. +/// +public sealed class LanguageSystem : SharedLanguageSystem +{ + [Dependency] private readonly IBaseClient _client = default!; + + /// + /// The current language of the entity currently possessed by the player. + /// + public string CurrentLanguage { get; private set; } = default!; + /// + /// The list of languages the currently possessed entity can speak. + /// + public List SpokenLanguages { get; private set; } = new(); + /// + /// The list of languages the currently possessed entity can understand. + /// + public List UnderstoodLanguages { get; private set; } = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnLanguagesUpdated); + _client.RunLevelChanged += OnRunLevelChanged; + } + + private void OnLanguagesUpdated(LanguagesUpdatedMessage message) + { + CurrentLanguage = message.CurrentLanguage; + SpokenLanguages = message.Spoken; + UnderstoodLanguages = message.Understood; + } + + private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs args) + { + // Request an update when entering a game + if (args.NewLevel == ClientRunLevel.InGame) + RequestStateUpdate(); + } + + /// + /// Sends a network request to the server to update this system's state. + /// The server may ignore the said request if the player is not possessing an entity. + /// + public void RequestStateUpdate() + { + RaiseNetworkEvent(new RequestLanguagesMessage()); + } + + public void RequestSetLanguage(LanguagePrototype language) + { + if (language.ID == CurrentLanguage) + return; + + RaiseNetworkEvent(new LanguagesSetMessage(language.ID)); + + // May cause some minor desync... + // So to reduce the probability of desync, we replicate the change locally too + if (SpokenLanguages.Contains(language.ID)) + CurrentLanguage = language.ID; + } +} diff --git a/Content.Client/Language/Systems/TranslatorImplanterSystem.cs b/Content.Client/Language/Systems/TranslatorImplanterSystem.cs new file mode 100644 index 00000000000..da19b3decf9 --- /dev/null +++ b/Content.Client/Language/Systems/TranslatorImplanterSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Language.Systems; + +namespace Content.Client.Language.Systems; + +public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem +{ + +} diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index f0537079b97..49e8099e0fb 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -212,6 +212,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action + + + +