#include "SUDSMessageLogger.h" #include "SUDSScript.h" #include "Misc/AutomationTest.h" #include "SUDSScriptImporter.h" #include "SUDSScriptNode.h" #include "TestUtils.h" UE_DISABLE_OPTIMIZATION const FString SimpleParsingInput = R"RAWSUD( === # Nothing in header but a comment === # A comment in body Player: Excuse me? NPC: Well, hello there. This is a test. * A test? NPC: Yes, a test. This is some indented continuation text. Player: Oh I see, thank you. NPC: You're welcome. * Another option NPC: This is another option with an embedded choice. * How far can this go? NPC: Theoretically forever but who knows? * This is an extra question NPC: That should have been added to the previous choice * Another question? NPC: Yep, this one too * A third level of questions? NPC: Yes, really! * Wow NPC: IKR * Continuation with no response, just fallthrough Player: This is a level 2 fallthrough Player: Well, that's all for now. This should appear for all paths as a fall-through. This, in fact, is a multi-line piece of text Which is joined to the previous text node with the line breaks NPC: Bye! )RAWSUD"; const FString GotoParsingInput = R"RAWSUD( :start :alsostart Player: This is the start :choice * Go to end NPC: How rude, bye then [goto end] * Nested option NPC: Some nested text with formatting * Go to goodbye Player: Gotta go! [go to goodbye] * Skip [goto secondchoice] * This is a mistake NPC: Oh no [goto this_is_an_error] :secondchoice NPC: Yep, this one too * Go back to choice NPC: Okay! [goto choice] * Return to the start NPC: Gotcha [goto start] * Alternative start, also with no text before [goto alsostart] :goodbye NPC: Bye! )RAWSUD"; const FString SetVariableParsingInput = R"RAWSUD( === # Set some vars in header # Text var with an existing localised ID [set SpeakerName.Player "Protagonist"] @12345@ # Text var no localised ID [set ValetName "Bob"] [set SomeFloat 12.5] [set SomeName `AName`] [set EmbeddedQuoteString "Hello this has some \"Embedded Quotes\""] === Player: Hello [set SomeInt 99] NPC: Wotcha # Test that inserting a set node in between text and choice doesn't break link [set SomeGender masculine] * Choice 1 [set SomeBoolean True] NPC: Truth * Choice 2 NPC: Surprise [set ValetName "Kate"] [set SomeGender feminine] Player: Well )RAWSUD"; const FString ConditionalParsingInput = R"RAWSUD( === # Nothing in header but a comment === # A comment in body Player: Excuse me? NPC: Well, hello there. This is a test. * A test? NPC: Yes, a test. This is some indented continuation text. Player: Oh I see, thank you. [if $polite == 1] NPC: You're welcome. [else] NPC: Too right. [endif] * Another option NPC: This is another option with an embedded choice. * How far can this go? NPC: Theoretically forever but who knows? [if $extraq] * This is an extra question NPC: That should have been added to the previous choice * Another question? NPC: Yep, this one too [endif] Player: Well, that's all for now. This should appear for all paths as a fall-through. This, in fact, is a multi-line piece of text Which is joined to the previous text node with the line breaks NPC: Bye! )RAWSUD"; IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSimpleParsing, "SUDSTest.TestSimpleParsing", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestSimpleParsing::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SimpleParsingInput), SimpleParsingInput.Len(), "SimpleParsingInput", &Logger, true)); // Test the content of the parsing auto RootNode = Importer.GetNode(0); if (!TestNotNull("Root node should exist", RootNode)) return false; TestEqual("Root node type", RootNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Root node speaker", RootNode->Identifier, "Player"); TestEqual("Root node text", RootNode->Text, "Excuse me?"); TestEqual("Root node path", RootNode->ChoicePath, "/"); TestEqual("Root node edges", RootNode->Edges.Num(), 1); auto NextNode = Importer.GetNode(RootNode->Edges[0].TargetNodeIdx); if (!TestNotNull("Next node should exist", NextNode)) return false; TestEqual("Second node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Second node speaker", NextNode->Identifier, "NPC"); TestEqual("Second node text", NextNode->Text, "Well, hello there. This is a test."); TestEqual("Second node path", RootNode->ChoicePath, "/"); TestEqual("Second node edges", NextNode->Edges.Num(), 1); NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (!TestNotNull("Third node should exist", NextNode)) return false; TestEqual("Third node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); // Choice itself is still at root, only the edges (individual choices) introduce new path levels TestEqual("Third node path", NextNode->ChoicePath, "/"); TestEqual("Third node edges", NextNode->Edges.Num(), 2); auto Choice1Node = NextNode; const FSUDSParsedNode* FallthroughNode = nullptr; if (NextNode->Edges.Num() >= 2) { TestEqual("Choice 1 node edge 0 text", Choice1Node->Edges[0].Text, "A test?"); TestEqual("Choice 1 node edge 1 text", Choice1Node->Edges[1].Text, "Another option"); // Follow choice 1 NextNode = Importer.GetNode(Choice1Node->Edges[0].TargetNodeIdx); if (TestNotNull("Next node should exist", NextNode)) { TestEqual("Choice 1 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Choice 1 1st text node speaker", NextNode->Identifier, "NPC"); TestEqual("Choice 1 1st text node text", NextNode->Text, "Yes, a test. This is some indented continuation text."); TestEqual("Choice 1 1st text node path", NextNode->ChoicePath, "/C001/"); TestEqual("Choice 1 1st text node edges", NextNode->Edges.Num(), 1); NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Next node should exist", NextNode)) { TestEqual("Choice 1 2nd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Choice 1 2nd text node speaker", NextNode->Identifier, "Player"); TestEqual("Choice 1 2nd text node text", NextNode->Text, "Oh I see, thank you."); TestEqual("Choice 1 2nd text node path", NextNode->ChoicePath, "/C001/"); TestEqual("Choice 1 2nd text node edges", NextNode->Edges.Num(), 1); NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Next node should exist", NextNode)) { TestEqual("Choice 1 3rd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Choice 1 3rd text node speaker", NextNode->Identifier, "NPC"); TestEqual("Choice 1 3rd text node text", NextNode->Text, "You're welcome."); TestEqual("Choice 1 3rd text node path", NextNode->ChoicePath, "/C001/"); // Should fall through, all the way to the end and not to "level 2 fallthrough" since that's deeper level TestEqual("Choice 1 3rd text node edges", NextNode->Edges.Num(), 1); if (NextNode->Edges.Num() >= 1) { auto LinkedNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Choice 1 3rd text linked node", LinkedNode)) { TestTrue("Choice 1 3rd text target node", LinkedNode->Text.StartsWith("Well, that's all for now")); FallthroughNode = LinkedNode; } } } } } // Follow choice 2 NextNode = Importer.GetNode(Choice1Node->Edges[1].TargetNodeIdx); if (!TestNotNull("Next node should exist", NextNode)) return false; TestEqual("Choice 2 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Choice 2 1st text node speaker", NextNode->Identifier, "NPC"); TestEqual("Choice 2 1st text node text", NextNode->Text, "This is another option with an embedded choice."); TestEqual("Choice 2 2nd text node path", NextNode->ChoicePath, "/C002/"); TestEqual("Choice 2 1st text node edges", NextNode->Edges.Num(), 1); NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (!TestNotNull("Next node should exist", NextNode)) return false; // This is nested choice node TestEqual("Choice 2 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); TestEqual("Choice 2 nested choice edges", NextNode->Edges.Num(), 3); auto NestedChoiceNode = NextNode; if (NestedChoiceNode->Edges.Num() >= 3) { TestEqual("Nested choice edge text 0", NestedChoiceNode->Edges[0].Text, "How far can this go?"); NextNode = Importer.GetNode(NestedChoiceNode->Edges[0].TargetNodeIdx); if (TestNotNull("Next node should exist", NextNode)) { TestEqual("Nested Choice 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Nested Choice 1st text node speaker", NextNode->Identifier, "NPC"); TestEqual("Nested Choice 1st text node text", NextNode->Text, "Theoretically forever but who knows?"); // Choice edges are assigned unique numbers in ascending order, but nested // This helps with fallthrough TestEqual("Nested Choice 1st text node path", NextNode->ChoicePath, "/C002/C003/"); if (TestEqual("Nested Choice 1st text node edges", NextNode->Edges.Num(), 1)) { // Should fall through auto LinkedNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Nested Choice 1st linked node", LinkedNode)) { TestEqual("Nested Choice 1st text target node", LinkedNode->Text, "This is a level 2 fallthrough"); } } } TestEqual("Nested choice edge text 1", NestedChoiceNode->Edges[1].Text, "This is an extra question"); NextNode = Importer.GetNode(NestedChoiceNode->Edges[1].TargetNodeIdx); if (TestNotNull("Next node should exist", NextNode)) { TestEqual("Nested Choice 2nd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Nested Choice 2nd text node speaker", NextNode->Identifier, "NPC"); TestEqual("Nested Choice 2nd text node text", NextNode->Text, "That should have been added to the previous choice"); TestEqual("Nested Choice 2nd text node path", NextNode->ChoicePath, "/C002/C004/"); TestEqual("Nested Choice 2nd text node edges", NextNode->Edges.Num(), 1); if (TestEqual("Nested Choice 2nd text node edges", NextNode->Edges.Num(), 1)) { // Should fall through auto LinkedNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Nested Choice 2nd linked node", LinkedNode)) { TestEqual("Nested Choice 2nd text target node", LinkedNode->Text, "This is a level 2 fallthrough"); } } } TestEqual("Nested choice edge text 2", NestedChoiceNode->Edges[2].Text, "Another question?"); NextNode = Importer.GetNode(NestedChoiceNode->Edges[2].TargetNodeIdx); if (TestNotNull("Next node should exist", NextNode)) { TestEqual("Nested Choice 3rd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Nested Choice 3rd text node speaker", NextNode->Identifier, "NPC"); TestEqual("Nested Choice 3rd text node text", NextNode->Text, "Yep, this one too"); TestEqual("Nested Choice 3rd text node path", NextNode->ChoicePath, "/C002/C005/"); if (TestEqual("Nested Choice 3rd text node edges", NextNode->Edges.Num(), 1)) { // Double nested auto DoubleChoiceNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Double Nested Choice node", DoubleChoiceNode)) { TestEqual("Double Nested Choice node type", DoubleChoiceNode->NodeType, ESUDSParsedNodeType::Choice); if (TestEqual("Double Nested Choice node edges", DoubleChoiceNode->Edges.Num(), 3)) { TestEqual("Double Nested choice edge text 0", DoubleChoiceNode->Edges[0].Text, "A third level of questions?"); NextNode = Importer.GetNode(DoubleChoiceNode->Edges[0].TargetNodeIdx); if (TestNotNull("Double Nested Choice node option 0", NextNode)) { TestEqual("Double Nested Choice option 0 text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Double Nested Choice option 0 text node speaker", NextNode->Identifier, "NPC"); TestEqual("Double Nested Choice option 0 text node text", NextNode->Text, "Yes, really!"); // Should fall through if (TestEqual("Double Nested Choice option 0 text edge", NextNode->Edges.Num(), 1)) NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Double Nested Choice option 0 text linked node", NextNode)) { TestEqual("Double Nested Choice option 0 text linked node", NextNode->Text, "This is a level 2 fallthrough"); } } TestEqual("Double Nested choice edge text 1", DoubleChoiceNode->Edges[1].Text, "Wow"); NextNode = Importer.GetNode(DoubleChoiceNode->Edges[1].TargetNodeIdx); if (TestNotNull("Double Nested Choice node option 1", NextNode)) { TestEqual("Double Nested Choice option 1 text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Double Nested Choice option 1 text node speaker", NextNode->Identifier, "NPC"); TestEqual("Double Nested Choice option 1 text node text", NextNode->Text, "IKR"); // Should fall through if (TestEqual("Double Nested Choice option 1 text edge", NextNode->Edges.Num(), 1)) NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Double Nested Choice option 0 text linked node", NextNode)) { TestEqual("Double Nested Choice option 0 text linked node", NextNode->Text, "This is a level 2 fallthrough"); } } // Last Should fall through TestEqual("Double Nested choice edge text 2", DoubleChoiceNode->Edges[2].Text, "Continuation with no response, just fallthrough"); auto LinkedNode = Importer.GetNode(DoubleChoiceNode->Edges[2].TargetNodeIdx); if (TestNotNull("Nested Choice 3rd linked node", LinkedNode)) { TestEqual("Nested Choice 3rd text target node", LinkedNode->Text, "This is a level 2 fallthrough"); } } } } } } if (TestNotNull("Should have found fallthrough node", FallthroughNode)) { // Test the final fallthrough TestEqual("Fallthrough node type", FallthroughNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Fallthrough node speaker", FallthroughNode->Identifier, "Player"); TestEqual("Fallthrough node text", FallthroughNode->Text, "Well, that's all for now. This should appear for all paths as a fall-through.\nThis, in fact, is a multi-line piece of text\nWhich is joined to the previous text node with the line breaks"); if (TestEqual("Fallthrough node edge count", FallthroughNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(FallthroughNode->Edges[0].TargetNodeIdx); if (TestNotNull("Fallthrough node next node not null", NextNode)) { TestEqual("Fallthrough node 2 type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Fallthrough node 2 speaker", NextNode->Identifier, "NPC"); TestEqual("Fallthrough node 2 text", NextNode->Text, "Bye!"); // Should have no further edges since is at end TestEqual("Fallthrough node 2 edge count", NextNode->Edges.Num(), 0); } } } } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoParsing, "SUDSTest.TestGotoParsing", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestGotoParsing::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoParsingInput), GotoParsingInput.Len(), "GotoParsingInput", &Logger, true)); // Test the content of the parsing auto RootNode = Importer.GetNode(0); if (!TestNotNull("Root node should exist", RootNode)) return false; TestEqual("Root node type", RootNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Root node speaker", RootNode->Identifier, "Player"); TestEqual("Root node text", RootNode->Text, "This is the start"); TestEqual("Root node edges", RootNode->Edges.Num(), 1); auto NextNode = Importer.GetNode(RootNode->Edges[0].TargetNodeIdx); if (!TestNotNull("Next node should exist", NextNode)) return false; TestEqual("Choice node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); if (TestEqual("Choice node edges", NextNode->Edges.Num(), 2)) { auto ChoiceNode = NextNode; TestEqual("Choice 1 text", ChoiceNode->Edges[0].Text, "Go to end"); NextNode = Importer.GetNode(ChoiceNode->Edges[0].TargetNodeIdx); if (TestNotNull("Next node should not be null", NextNode)) { TestEqual("Goto End node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Goto End node text speaker", NextNode->Identifier, "NPC"); TestEqual("Goto End node text text", NextNode->Text, "How rude, bye then"); if (TestEqual("Goto End node text edges", NextNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Goto node should not be null", NextNode)) { TestEqual("Goto End node type", NextNode->NodeType, ESUDSParsedNodeType::Goto); TestEqual("Goto End node label", NextNode->Identifier, FSUDSScriptImporter::EndGotoLabel); } } } TestEqual("Choice 2 text", ChoiceNode->Edges[1].Text, "Nested option"); NextNode = Importer.GetNode(ChoiceNode->Edges[1].TargetNodeIdx); if (TestNotNull("Next node should not be null", NextNode)) { TestEqual("Choice 2 node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Choice 2 node text speaker", NextNode->Identifier, "NPC"); TestEqual("Choice 2 node text text", NextNode->Text, "Some nested text with formatting"); if (TestEqual("Choice 2 node text edges", NextNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Nested choice node should not be null", NextNode)) { TestEqual("Goto End node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); auto NestedChoice = NextNode; if (TestEqual("Nested Choice node edges", NextNode->Edges.Num(), 3)) { TestEqual("Nested choice 0 text", NestedChoice->Edges[0].Text, "Go to goodbye"); NextNode = Importer.GetNode(NestedChoice->Edges[0].TargetNodeIdx); if (TestNotNull("Should not be null", NextNode)) { TestEqual("Nested choice 0 text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Nested choice 0 text speaker", NextNode->Identifier, "Player"); TestEqual("Nested choice 0 text text", NextNode->Text, "Gotta go!"); if (TestEqual("Nested Choice textnode edges", NextNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Should not be null", NextNode)) { // This is the goto TestEqual("Nested choice 0 goto type", NextNode->NodeType, ESUDSParsedNodeType::Goto); TestEqual("Nested choice 0 goto label", NextNode->Identifier, "goodbye"); const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); TestNotEqual("Label should be valid", DestIdx, -1); NextNode = Importer.GetNode(DestIdx); if (TestNotNull("Goto dest node", NextNode)) { TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Goto dest text speaker", NextNode->Identifier, "NPC"); TestEqual("Goto dest text text", NextNode->Text, "Bye!"); } } } } TestEqual("Nested choice 1 text", NestedChoice->Edges[1].Text, "Skip"); NextNode = Importer.GetNode(NestedChoice->Edges[1].TargetNodeIdx); if (TestNotNull("Should not be null", NextNode)) { // This one goes straight to goto TestEqual("Nested choice 1 goto type", NextNode->NodeType, ESUDSParsedNodeType::Goto); TestEqual("Nested choice 1 goto label", NextNode->Identifier, "secondchoice"); const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); TestNotEqual("Label should be valid", DestIdx, -1); NextNode = Importer.GetNode(DestIdx); if (TestNotNull("Goto dest node", NextNode)) { TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Goto dest text speaker", NextNode->Identifier, "NPC"); TestEqual("Goto dest text text", NextNode->Text, "Yep, this one too"); } } TestEqual("Nested choice 2 text", NestedChoice->Edges[2].Text, "This is a mistake"); NextNode = Importer.GetNode(NestedChoice->Edges[2].TargetNodeIdx); if (TestNotNull("Should not be null", NextNode)) { TestEqual("Nested choice 2 text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Nested choice 2 text speaker", NextNode->Identifier, "NPC"); TestEqual("Nested choice 2 text text", NextNode->Text, "Oh no"); if (TestEqual("Nested Choice text node edges", NextNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("Should not be null", NextNode)) { // This is the goto TestEqual("Nested choice 0 goto type", NextNode->NodeType, ESUDSParsedNodeType::Goto); TestEqual("Nested choice 0 goto label", NextNode->Identifier, "this_is_an_error"); TestEqual("Label should go nowhere", Importer.GetGotoTargetNodeIndex(NextNode->Identifier), -1); } } } } } } } // Pick up the latter part (only reachable by goto) NextNode = Importer.GetNode(Importer.GetGotoTargetNodeIndex("secondchoice")); if (TestNotNull("End choice node should not be null", NextNode)) { TestEqual("End choice node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("End choice node text speaker", NextNode->Identifier, "NPC"); TestEqual("End choice node text text", NextNode->Text, "Yep, this one too"); if (TestEqual("End choice node text edges", NextNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("", NextNode)) { TestEqual("End choice node text type", NextNode->NodeType, ESUDSParsedNodeType::Choice); if (TestEqual("End choice node edges", NextNode->Edges.Num(), 3)) { auto EndChoiceNode = NextNode; TestEqual("End choice 0 text", EndChoiceNode->Edges[0].Text, "Go back to choice"); NextNode = Importer.GetNode(EndChoiceNode->Edges[0].TargetNodeIdx); if (TestNotNull("", NextNode)) { TestEqual("Node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Node text speaker", NextNode->Identifier, "NPC"); TestEqual("Node text text", NextNode->Text, "Okay!"); if (TestEqual("Next node edges", NextNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("", NextNode)) { TestEqual("Should be goto node", NextNode->NodeType, ESUDSParsedNodeType::Goto); TestEqual("Goto label", NextNode->Identifier, "choice"); // This should lead back to a choice node, ie letting the previous text node use the same choices // without repeating the text (loop with context) const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); TestNotEqual("Label should be valid", DestIdx, -1); NextNode = Importer.GetNode(DestIdx); if (TestNotNull("Goto dest node", NextNode)) { TestEqual("Goto dest choice type", NextNode->NodeType, ESUDSParsedNodeType::Choice); // Just make sure it's the right choice node if (TestEqual("Goto dest choice edges", NextNode->Edges.Num(), 2)) { TestEqual("Goto dest choice check edge", NextNode->Edges[0].Text, "Go to end"); } } } } NextNode = Importer.GetNode(EndChoiceNode->Edges[1].TargetNodeIdx); if (TestNotNull("", NextNode)) { TestEqual("Node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Node text speaker", NextNode->Identifier, "NPC"); TestEqual("Node text text", NextNode->Text, "Gotcha"); if (TestEqual("Next node edges", NextNode->Edges.Num(), 1)) { NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); if (TestNotNull("", NextNode)) { TestEqual("Should be goto node", NextNode->NodeType, ESUDSParsedNodeType::Goto); TestEqual("Goto label", NextNode->Identifier, "start"); // This should lead back to a choice node, ie letting the previous text node use the same choices // without repeating the text (loop with context) const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); TestNotEqual("Label should be valid", DestIdx, -1); NextNode = Importer.GetNode(DestIdx); if (TestNotNull("Goto dest node", NextNode)) { // Just make sure it's the right node TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Node text speaker", NextNode->Identifier, "Player"); TestEqual("Node text text", NextNode->Text, "This is the start"); } } } } NextNode = Importer.GetNode(EndChoiceNode->Edges[2].TargetNodeIdx); if (TestNotNull("", NextNode)) { // Straight to goto TestEqual("Should be goto node", NextNode->NodeType, ESUDSParsedNodeType::Goto); TestEqual("Goto label", NextNode->Identifier, "alsostart"); // This should lead back to a choice node, ie letting the previous text node use the same choices // without repeating the text (loop with context) const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); TestNotEqual("Label should be valid", DestIdx, -1); NextNode = Importer.GetNode(DestIdx); if (TestNotNull("Goto dest node", NextNode)) { // Just make sure it's the right node TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Node text speaker", NextNode->Identifier, "Player"); TestEqual("Node text text", NextNode->Text, "This is the start"); } } } } } } } // Test that start & autostart point to the same place const int StartNodeIdx = Importer.GetGotoTargetNodeIndex("start"); const auto StartNode = Importer.GetNode(StartNodeIdx); if (TestNotNull("Second choice node should not be null", StartNode)) { TestEqual("Start node text type", StartNode->NodeType, ESUDSParsedNodeType::Text); TestEqual("Start node text speaker", StartNode->Identifier, "Player"); TestEqual("Start node text text", StartNode->Text, "This is the start"); } const int AlsoStartNodeIdx = Importer.GetGotoTargetNodeIndex("alsostart"); TestEqual("start and alsostart should reference the same node", AlsoStartNodeIdx, StartNodeIdx); } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSetVariableParsing, "SUDSTest.TestSetVariableParsing", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestSetVariableParsing::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SetVariableParsingInput), SetVariableParsingInput.Len(), "SetVariableParsingInput", &Logger, true)); // Test the content of the parsing // Header nodes first auto NextNode = Importer.GetHeaderNode(0); TestParsedSetLiteral(this, "Header node 1", NextNode, "SpeakerName.Player", "Protagonist"); TestGetParsedNextNode(this, "Header node 1 next", NextNode, Importer, true, &NextNode); TestParsedSetLiteral(this, "Header node 2", NextNode, "ValetName", "Bob"); TestGetParsedNextNode(this, "Header node 2 next", NextNode, Importer, true, &NextNode); TestParsedSetLiteral(this, "Header node 3", NextNode, "SomeFloat", 12.5f); TestGetParsedNextNode(this, "Header node 3 next", NextNode, Importer, true, &NextNode); TestParsedSetLiteral(this, "Header node 4", NextNode, "SomeName", FName("AName")); TestGetParsedNextNode(this, "Header node 4 next", NextNode, Importer, true, &NextNode); TestParsedSetLiteral(this, "Header node 5", NextNode, "EmbeddedQuoteString", "Hello this has some \"Embedded Quotes\""); // Now body nodes NextNode = Importer.GetNode(0); TestParsedText(this, "Root node", NextNode, "Player", "Hello"); TestGetParsedNextNode(this, "Node 1 next", NextNode, Importer, false, &NextNode); TestParsedSetLiteral(this, "Node 2", NextNode, "SomeInt", 99); TestGetParsedNextNode(this, "Node 2 next", NextNode, Importer, false, &NextNode); TestParsedText(this, "Node 3 text", NextNode, "NPC", "Wotcha"); TestGetParsedNextNode(this, "Node 3 next", NextNode, Importer, false, &NextNode); TestParsedSetLiteral(this, "Node 4", NextNode, "SomeGender", ETextGender::Masculine); TestGetParsedNextNode(this, "Node 4 next", NextNode, Importer, false, &NextNode); TestParsedChoice(this, "Node 5 choice", NextNode, 2); auto ChoiceNode = NextNode; TestParsedChoiceEdge(this, "Choice 1 edge", ChoiceNode, 0, "Choice 1", Importer, &NextNode); TestParsedSetLiteral(this, "Choice 1 set", NextNode, "SomeBoolean", true); TestGetParsedNextNode(this, "Choice 1 next", NextNode, Importer, false, &NextNode); TestParsedText(this, "Choice 1 text", NextNode, "NPC", "Truth"); TestParsedChoiceEdge(this, "Choice 2 edge", ChoiceNode, 1, "Choice 2", Importer, &NextNode); TestParsedText(this, "Choice 2 text", NextNode, "NPC", "Surprise"); TestGetParsedNextNode(this, "Choice 2 next", NextNode, Importer, false, &NextNode); TestParsedSetLiteral(this, "Choice 2 set", NextNode, "ValetName", "Kate"); TestGetParsedNextNode(this, "Choice 2 next 2", NextNode, Importer, false, &NextNode); TestParsedSetLiteral(this, "Choice 2 set 2", NextNode, "SomeGender", ETextGender::Feminine); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestConversionToRuntime, "SUDSTest.TestConversionToRuntime", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestConversionToRuntime::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoParsingInput), GotoParsingInput.Len(), "GotoParsingInput", &Logger, true)); auto Asset = NewObject(GetTransientPackage(), "Test"); const ScopedStringTableHolder StringTableHolder; Importer.PopulateAsset(Asset, StringTableHolder.StringTable); auto StartNode = Asset->GetFirstNode(); if (!TestNotNull("Start node should be true", StartNode)) { return false; } TestTextNode(this, "Start node", StartNode, "Player", "This is the start"); auto NextNode = StartNode; if (!TestEqual("Start node edges", NextNode->GetEdgeCount(), 1)) { return false; } auto pEdge = NextNode->GetEdge(0); if (!TestNotNull("Start node edge", pEdge)) { return false; } NextNode = pEdge->GetTargetNode().Get(); if (TestChoiceNode(this, "Start node next", NextNode, 2)) { auto ChoiceNode = NextNode; if (TestChoiceEdge(this, "Choice 1 text", ChoiceNode, 0, "Go to end", &NextNode)) { TestTextNode(this, "Goto End node text type", NextNode, "NPC", "How rude, bye then"); // This is a goto end so should just have one edge auto GotoEdge = NextNode->GetEdge(0); if (TestNotNull("Goto end node", GotoEdge)) { TestFalse("Goto end should be null", GotoEdge->GetTargetNode().IsValid()); } } if (TestChoiceEdge(this, "Choice 2 text", ChoiceNode, 1, "Nested option", &NextNode)) { TestTextNode(this, "Choice 2 node text type", NextNode, "NPC", "Some nested text with formatting"); if (TestEdge(this, "Choice 2 node text edges", NextNode, 0, &NextNode)) { if (TestChoiceNode(this, "Goto End node type", NextNode, 3)) { auto NestedChoice = NextNode; if (TestChoiceEdge(this, "Nested choice 0 text", NestedChoice, 0, "Go to goodbye", &NextNode)) { TestTextNode(this, "Nested choice 0 text type", NextNode, "Player", "Gotta go!"); if (TestEdge(this, "Nested Choice textnode edges", NextNode, 0, &NextNode)) { // This was the goto which should have led to goodbye TestTextNode(this, "Check goto goodbye", NextNode, "NPC", "Bye!"); } } if (TestChoiceEdge(this, "Nested choice 1 text", NestedChoice, 1, "Skip", &NextNode)) { // This will go directly to secondchoice node TestTextNode(this, "Nested choice 1 goto", NextNode, "NPC", "Yep, this one too"); } if (TestChoiceEdge(this, "Nested choice 2 text", NestedChoice, 2, "This is a mistake", &NextNode)) { // there's a text node then a failed goto (nowhere) TestTextNode(this, "Nested choice 2 node", NextNode, "NPC", "Oh no"); auto GotoEdge = NextNode->GetEdge(0); if (TestNotNull("Nested choice 2 node edge", GotoEdge)) { TestFalse("Nested choice 2 node goes nowhere", GotoEdge->GetTargetNode().IsValid()); } } } } } // Pick up the latter part (only reachable by goto) NextNode = Asset->GetNodeByLabel("secondchoice"); if (TestTextNode(this, "secondchoice node", NextNode, "NPC", "Yep, this one too")) { if (TestEdge(this, "secondchoice node text edges", NextNode, 0, &NextNode)) { if (TestChoiceNode(this, "secondchoice next choice", NextNode, 3)) { auto EndChoiceNode = NextNode; if (TestChoiceEdge(this, "End choice 0 edge", EndChoiceNode, 0, "Go back to choice", &NextNode)) { TestTextNode(this, "Node text", NextNode, "NPC", "Okay!"); if (TestEdge(this, "Next node edges", NextNode, 0, &NextNode)) { // Should go back to :choice node TestChoiceNode(this, "Check choice goto", NextNode, 2); TestChoiceEdge(this, "Check choice goto", NextNode, 0, "Go to end", &NextNode); } } if (TestChoiceEdge(this, "End choice 1 edge", EndChoiceNode, 1, "Return to the start", &NextNode)) { TestTextNode(this, "Node text", NextNode, "NPC", "Gotcha"); if (TestEdge(this, "Next node edges", NextNode, 0, &NextNode)) { // Should go back to :start TestTextNode(this, "Check goto text", NextNode, "Player", "This is the start"); } } if (TestChoiceEdge(this, "End choice 2 edge", EndChoiceNode, 2, "Alternative start, also with no text before", &NextNode)) { // Should go directly back to :alsostart, same as :start TestTextNode(this, "Check goto text", NextNode, "Player", "This is the start"); } } } } } // check goto labels auto GotoNode = Asset->GetNodeByLabel("start"); TestTextNode(this, "Goto Start", GotoNode, "Player", "This is the start"); GotoNode = Asset->GetNodeByLabel("alsostart"); TestTextNode(this, "Goto alsostart", GotoNode, "Player", "This is the start"); GotoNode = Asset->GetNodeByLabel("choice"); TestChoiceNode(this, "Goto choice", GotoNode, 2); GotoNode = Asset->GetNodeByLabel("secondchoice"); TestTextNode(this, "Goto secondchoice", GotoNode, "NPC", "Yep, this one too"); GotoNode = Asset->GetNodeByLabel("goodbye"); TestTextNode(this, "Goto goodbye", GotoNode, "NPC", "Bye!"); // Test speakers TestEqual("Num speakers", Asset->GetSpeakers().Num(), 2); TestTrue("Speaker 1", Asset->GetSpeakers().Contains("Player")); TestTrue("Speaker 2", Asset->GetSpeakers().Contains("NPC")); return true; } const FString PartiallyLocalisedInput = R"RAWSUD( Vagabond: Well met, fellow! * Er, hi? Vagabond: Verily, 'tis wondrous to see such a fine fellow on the road this morn! [goto FriendlyChat] * Jog on, mate @0001@ Vagabond: Well, really! Good day then sir! @0002@ [goto end] :FriendlyChat Vagabond: Mayhaps we could travel together a while, and share a tale or two? @0007@ What do you say? )RAWSUD"; IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestPartiallyLocalised, "SUDSTest.TestPartiallyLocalised", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestPartiallyLocalised::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(PartiallyLocalisedInput), PartiallyLocalisedInput.Len(), "PartiallyLocalisedInput", &Logger, true)); // Test the content of the parsing auto NextNode = Importer.GetNode(0); TestParsedText(this, "Start node", NextNode, "Vagabond", "Well met, fellow!"); // Test that we generated unique textIDs for inserted lines that are unique // Should be after the last explicit one TestFalse("TextID should be populated", NextNode->TextID.IsEmpty()); TestEqual("TextID should be correct", NextNode->TextID, "@0008@"); TestGetParsedNextNode(this, "Next", NextNode, Importer, false, &NextNode); if (TestParsedChoice(this, "First choice", NextNode, 2)) { TestFalse("TextID should be populated", NextNode->Edges[0].TextID.IsEmpty()); TestEqual("TextID should be correct", NextNode->Edges[0].TextID, "@0009@"); TestFalse("TextID should be populated", NextNode->Edges[1].TextID.IsEmpty()); TestEqual("TextID should be correct", NextNode->Edges[1].TextID, "@0001@"); TestParsedChoiceEdge(this, "First choice", NextNode, 0, "Er, hi?", Importer, &NextNode); TestParsedText(this, "Next node", NextNode, "Vagabond", "Verily, 'tis wondrous to see such a fine fellow on the road this morn!"); TestFalse("TextID should be populated", NextNode->TextID.IsEmpty()); TestEqual("TextID should be correct", NextNode->TextID, "@000a@"); TestGetParsedNextNode(this, "Next", NextNode, Importer, false, &NextNode); TestParsedGoto(this, "Goto", NextNode, Importer, &NextNode); TestParsedText(this, "Next node", NextNode, "Vagabond", "Mayhaps we could travel together a while, and share a tale or two?\nWhat do you say?"); TestEqual("TextID should be correct", NextNode->TextID, "@0007@"); } NextNode = Importer.GetNode(0); TestGetParsedNextNode(this, "Next", NextNode, Importer, false, &NextNode); if (TestParsedChoice(this, "First choice", NextNode, 2)) { TestEqual("TextID should be correct", NextNode->Edges[1].TextID, "@0001@"); TestParsedChoiceEdge(this, "First choice", NextNode, 1, "Jog on, mate", Importer, &NextNode); TestParsedText(this, "Next node", NextNode, "Vagabond", "Well, really! Good day then sir!"); TestEqual("TextID should be correct", NextNode->TextID, "@0002@"); } return true; } const FString ProblemChoiceInput = R"RAWSUD( NPC: Well, hello there. This is a test. :choice * A test? Player: Yes! * This is a mistake; goto goes direct to another choice [goto choice] NPC: Bye! )RAWSUD"; IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestParseChoiceProblem, "SUDSTest.TestParseChoiceProblem", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestParseChoiceProblem::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestFalse("Import should fail", Importer.ImportFromBuffer(GetData(ProblemChoiceInput), ProblemChoiceInput.Len(), "ProblemChoiceInput", &Logger, true)); if (TestTrue("Logger should have registered errors", Logger.HasErrors())) { FText ErrMsg = Logger.GetErrorMessages()[0]->ToText(); TestTrue("Error should contain 'Choices MUST show another speaker line'", ErrMsg.ToString().Contains("Choices MUST show another speaker line")); } return true; } const FString TrailingEventInput = R"RAWSUD( NPC: Well, hello there. This is a test. * Choice [event SomeEvent 1] )RAWSUD"; IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestTrailingEventProblem, "SUDSTest.TestTrailingEventProblem", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestTrailingEventProblem::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(TrailingEventInput), TrailingEventInput.Len(), "TrailingEventInput", &Logger, true)); return true; } const FString ConditionalChoicesNestedAndSiblingsInput = R"RAWSUD( Player: Hello [if {TopCondition} == 0] * Top level choice 1 [goto outro] [endif] [if {TopCondition} == 1] * Top level choice 2 [goto outro] [if {NestedCondition1} == 1] * Nested condition choice 1 [goto outro] [endif] [if {NestedCondition2} == 0] * Nested condition choice 2 [goto outro] [endif] [if {NestedCondition3} == 1] * Nested condition choice 3 [goto outro] [endif] [endif] :outro Player: Bye )RAWSUD"; IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestConditionalChoicesNestedAndSiblings, "SUDSTest.TestConditionalChoicesNestedAndSiblings", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestConditionalChoicesNestedAndSiblings::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ConditionalChoicesNestedAndSiblingsInput), ConditionalChoicesNestedAndSiblingsInput.Len(), "ConditionalChoicesNestedAndSiblingsInput", &Logger, true)); auto Asset = NewObject(GetTransientPackage(), "Test"); const ScopedStringTableHolder StringTableHolder; Importer.PopulateAsset(Asset, StringTableHolder.StringTable); auto StartNode = Asset->GetFirstNode(); if (!TestNotNull("Start node should be true", StartNode)) { return false; } TestTextNode(this, "Start node", StartNode, "Player", "Hello"); auto NextNode = StartNode; if (!TestEqual("Start node edges", NextNode->GetEdgeCount(), 1)) { return false; } auto pEdge = NextNode->GetEdge(0); if (!TestNotNull("Start node edge", pEdge)) { return false; } /* Node structure: T -> C1 C1 -> S1 -> (if TopCondition == 0) C2 --> Top level choice 1 C1 -> S2 -> (if TopCondition == 1) C3 -> Top Level choice 2 |-> S3 -> (if NestedCondition1 == 1) -> C4 -> Nested condition choice 1 |-> S4 -> (if NestedCondition2 == 0) -> C5 -> Nested condition choice 2 |-> S5 -> (if NestedCondition3 == 0) -> C5 -> Nested condition choice 3 */ NextNode = pEdge->GetTargetNode().Get(); if (TestChoiceNode(this, "Start node next", NextNode, 2)) { auto ChoiceNode = NextNode; // Top condition == 0 subtree if (TestChoiceEdge(this, "Select Path 1", ChoiceNode, 0, "", &NextNode)) { if (TestSelectNode(this, "Select 1", NextNode, 1)) { if (TestSelectEdge(this, "Select 1 Edge", NextNode, 0, "{TopCondition} == 0", &NextNode)) { if (TestChoiceNode(this, "Choice 1 Leaf", NextNode, 1)) { TestChoiceEdge(this, "Choice 1 Leaf Edge", NextNode, 0, "Top level choice 1", &NextNode); } } } } // Top condition == 1 subtree if (TestChoiceEdge(this, "Select Path 2", ChoiceNode, 1, "", &NextNode)) { if (TestSelectNode(this, "Select 2", NextNode, 1)) { if (TestSelectEdge(this, "Select 2 Edge", NextNode, 0, "{TopCondition} == 1", &NextNode)) { // This choice node holds the non-nested condition choice, plus 3 more nested selects // Because each condition is a sibling they become separate select subtrees if (TestChoiceNode(this, "Choice 2 Mid", NextNode, 4)) { auto ChoiceNode2 = NextNode; TestChoiceEdge(this, "Choice 2 Leaf Edge 1", ChoiceNode2, 0, "Top level choice 2", &NextNode); if (TestChoiceEdge(this, "Choice 2 To Select Edge 1", ChoiceNode2, 1, "", &NextNode)) { if (TestSelectNode(this, "Select 2", NextNode, 1)) { if (TestSelectEdge(this, "Select 2 Edge", NextNode, 0, "{NestedCondition1} == 1", &NextNode)) { if (TestChoiceNode(this, "Choice 2 Leaf", NextNode, 1)) { TestChoiceEdge(this, "Choice 2 Leaf Edge", NextNode, 0, "Nested condition choice 1", &NextNode); } } } } if (TestChoiceEdge(this, "Choice 2 To Select Edge 2", ChoiceNode2, 2, "", &NextNode)) { if (TestSelectNode(this, "Select 3", NextNode, 1)) { if (TestSelectEdge(this, "Select 3 Edge", NextNode, 0, "{NestedCondition2} == 0", &NextNode)) { if (TestChoiceNode(this, "Choice 3 Leaf", NextNode, 1)) { TestChoiceEdge(this, "Choice 3 Leaf Edge", NextNode, 0, "Nested condition choice 2", &NextNode); } } } } if (TestChoiceEdge(this, "Choice 2 To Select Edge 3", ChoiceNode2, 3, "", &NextNode)) { if (TestSelectNode(this, "Select 4", NextNode, 1)) { if (TestSelectEdge(this, "Select 4 Edge", NextNode, 0, "{NestedCondition3} == 1", &NextNode)) { if (TestChoiceNode(this, "Choice 4 Leaf", NextNode, 1)) { TestChoiceEdge(this, "Choice 4 Leaf Edge", NextNode, 0, "Nested condition choice 3", &NextNode); } } } } } } } } } return true; } // The deliberate mistake here is that the speaker lines are not indented underneath the choices, // which when combined with the conditionals confuses the parser so that it leaves all but the // first choice orphaned and unreachable. Import should report this to the user const FString ErrorsReportedForBadChoiceIndentsInput = R"RAWSUD( npc: hello :chatchoices [if {npc_hasQuests}] * Any news? player: Any news? [endif] [if {npc_canTrade}] * Trade player: What do you have for sale? [endif] [if {npc_hasLore}] * How are you? player: How are things? [endif] * Nevermind npc: Alright. Come back anytime. )RAWSUD"; IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestErrorsReportedForBadChoiceIndents, "SUDSTest.TestErrorsReportedForBadChoiceIndents", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter) bool FTestErrorsReportedForBadChoiceIndents::RunTest(const FString& Parameters) { FSUDSMessageLogger Logger(false); FSUDSScriptImporter Importer; TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ErrorsReportedForBadChoiceIndentsInput), ErrorsReportedForBadChoiceIndentsInput.Len(), "ErrorsReportedForBadChoiceIndentsInput", &Logger, true)); TestTrue("Warnings should have been logged", Logger.HasWarnings()); return true; } UE_ENABLE_OPTIMIZATION