message:
Add miscellaneous tests
This PR adds some more miscellaneous tests for more code coverage.

diff:
@@ -94,13 +94,14 @@ class ExpressionEvaluatorTest : AbstractTest() {
// directly use the xtext classes
val convertedMark = MarkModelLoader().load(mapOf("" to model))
assertNotNull(convertedMark)
-
- val convertedRule = convertedMark.rules.first()
- assertNotNull(convertedRule)
+ assertNotNull(convertedMark.rules)
val convertedEntity = convertedMark.getEntity("MyClass")
assertNotNull(convertedEntity)
+ for (convertedRule in convertedMark.rules) {
+ assertNotNull(convertedRule)
+
val eval =
ExpressionEvaluator(
graph,
@@ -117,7 +118,38 @@ class ExpressionEvaluatorTest : AbstractTest() {
val ctx = result[0]
assertNotNull(ctx)
- assertEquals(ConstantValue.of(true), ctx)
+ assertEquals(
+ ConstantValue.of(true),
+ ctx,
+ "${convertedRule.name} was not evaluated to true"
+ )
+ }
+ }
+
+ @Test
+ fun nullExpressionTest() {
+ val evaluator = Evaluator(mark, ServerConfiguration.builder().build())
+
+ // Let's provide the list of entities and nodes, this is normally done by
+ // findInstancesForEntities. In this case, we simply point it towards our variable
+ // declaration
+ val entities = listOf(listOf(Pair("a", a)))
+
+ val markContextHolder = evaluator.createMarkContext(entities)
+ assertEquals(1, markContextHolder.allContexts.size)
+
+ val eval =
+ ExpressionEvaluator(
+ graph,
+ null,
+ null,
+ null,
+ ServerConfiguration.builder().build(),
+ markContextHolder
+ )
+
+ val result = eval.evaluateExpression(null)
+ assertEquals(markContextHolder.generateNullResult(), result)
}
companion object {
@@ -181,7 +213,18 @@ class ExpressionEvaluatorTest : AbstractTest() {
graph = result.graph
var myEntity: EntityDeclaration? = null
- var myRule: RuleDeclaration? = null
+ var eqRule: RuleDeclaration? = null
+ var multRule: RuleDeclaration? = null
+ var divRule: RuleDeclaration? = null
+ var modRule: RuleDeclaration? = null
+ var leftShiftRule: RuleDeclaration? = null
+ var rightShiftRule: RuleDeclaration? = null
+ var bitwiseAndRule: RuleDeclaration? = null
+ var bitwiseOrRule: RuleDeclaration? = null
+ var plusSignRule: RuleDeclaration? = null
+ var minusSignRule: RuleDeclaration? = null
+ var logicalNotRule: RuleDeclaration? = null
+ var bitwiseComplementRule: RuleDeclaration? = null
model = mark {
myEntity =
@@ -189,18 +232,174 @@ class ExpressionEvaluatorTest : AbstractTest() {
variable("field")
op("init") { stmt { call("MyClass") { param("field") } } }
}
- myRule =
- rule("myRule") {
+ eqRule =
+ rule("eqRule") {
statement {
using(myEntity!!, "a")
ensure {
+ // comparison is: a == 1
+ exp =
comparison(left = operand("a.field"), op = "==", right = lit(1))
}
}
}
+ multRule =
+ rule("multRule") {
+ statement {
+ using(myEntity!!, "a")
+ ensure {
+ // comparison is: a == 1 * 1
+ exp =
+ comparison(
+ left = operand("a.field"),
+ op = "==",
+ right = mul(left = lit(1), op = "*", right = lit(1))
+ )
+ }
+ }
+ }
+ divRule =
+ rule("divRule") {
+ statement {
+ using(myEntity!!, "a")
+ ensure {
+ // comparison is: a == 2 / 2
+ exp =
+ comparison(
+ left = operand("a.field"),
+ op = "==",
+ right = mul(left = lit(2), op = "/", right = lit(2))
+ )
+ }
+ }
+ }
+ modRule =
+ rule("modRule") {
+ statement {
+ using(myEntity!!, "a")
+ ensure {
+ // comparison is: a == 19 % 6
+ exp =
+ comparison(
+ left = operand("a.field"),
+ op = "==",
+ right = mul(left = lit(19), op = "%", right = lit(6))
+ )
+ }
+ }
+ }
+ leftShiftRule =
+ rule("leftShiftRule") {
+ statement {
+ ensure {
+ // comparison is: 32 == 2 << 4
+ exp =
+ comparison(
+ left = lit(32),
+ op = "==",
+ right = mul(left = lit(2), op = "<<", right = lit(4))
+ )
+ }
+ }
+ }
+ rightShiftRule =
+ rule("rightShiftRule") {
+ statement {
+ ensure {
+ // comparison is: 4 == 32 >> 3
+ exp =
+ comparison(
+ left = lit(4),
+ op = "==",
+ right = mul(left = lit(32), op = ">>", right = lit(3))
+ )
+ }
+ }
+ }
+ bitwiseAndRule =
+ rule("bitwiseAndRule") {
+ statement {
+ ensure {
+ // comparison is: 2 == 11 & 6 (0b10 == 0b1011 & 0b110)
+ exp =
+ comparison(
+ left = lit(2),
+ op = "==",
+ right = mul(left = lit(11), op = "&", right = lit(6))
+ )
+ }
+ }
+ }
+
+ bitwiseOrRule =
+ rule("bitwiseOrRule") {
+ statement {
+ ensure {
+ // comparison is: 5 == 7 & 2 (0b101 == 0b111 & 0b10)
+ exp =
+ comparison(
+ left = lit(5),
+ op = "==",
+ right = mul(left = lit(7), op = "&^", right = lit(2))
+ )
+ }
+ }
+ }
+ plusSignRule =
+ rule("plusSignRule") {
+ statement {
+ ensure {
+ // comparison is: 5 == +5
+ exp =
+ comparison(
+ left = lit(5),
+ op = "==",
+ right = unary(exp = lit(5), op = "+")
+ )
+ }
+ }
+ }
+ minusSignRule =
+ rule("minusSignRule") {
+ statement {
+ ensure {
+ // comparison is: 5 != -(5)
+ exp =
+ comparison(
+ left = lit(5),
+ op = "!=",
+ right = unary(exp = lit(5), op = "-")
+ )
+ }
+ }
+ }
+ logicalNotRule =
+ rule("logicalNotRule") {
+ statement {
+ ensure {
+ // comparison is: true == !false
+ exp =
+ comparison(
+ left = lit(true),
+ op = "==",
+ right = unary(exp = lit(false), op = "!")
+ )
+ }
+ }
+ }
}
- assertNotNull(myRule)
+ assertNotNull(eqRule)
+ assertNotNull(multRule)
+ assertNotNull(divRule)
+ assertNotNull(modRule)
+ assertNotNull(leftShiftRule)
+ assertNotNull(rightShiftRule)
+ assertNotNull(bitwiseAndRule)
+ assertNotNull(bitwiseOrRule)
+ assertNotNull(plusSignRule)
+ assertNotNull(minusSignRule)
+ assertNotNull(logicalNotRule)
assertNotNull(myEntity)
mark = MarkModelLoader().load(mapOf("" to model))

diff:
package de.fraunhofer.aisec.codyze.legacy.crymlin
+import de.fraunhofer.aisec.codyze.legacy.config.CodyzeConfiguration
+import de.fraunhofer.aisec.codyze.legacy.config.Configuration
+import de.fraunhofer.aisec.codyze.legacy.config.CpgConfiguration
import de.fraunhofer.aisec.cpg.TranslationConfiguration
import de.fraunhofer.aisec.cpg.TranslationManager
import de.fraunhofer.aisec.cpg.passes.EdgeCachePass
@@ -12,7 +15,7 @@ abstract class AbstractTest {
* Helper method for initializing an Analysis Run.
*
* @param sourceLocations
- * @return
+ * @return TranslationManager
*/
@JvmStatic
protected fun newAnalysisRun(vararg sourceLocations: File): TranslationManager {
@@ -30,5 +33,23 @@ abstract class AbstractTest {
)
.build()
}
+
+ /**
+ * Helper method for initializing an Analysis Run. Returned Configuration can be used to
+ * initialize AnalysisServer and start the analysis with {@link
+ * de.fraunhofer.aisec.codyze.legacy.analysis.AnalysisServer#analyze(String url)}.
+ *
+ * @param codyzeConfig
+ * @return Configuration
+ */
+ @JvmStatic
+ protected fun newAnalysisRun(codyzeConfig: CodyzeConfiguration): Configuration {
+ val cpgConfig = CpgConfiguration()
+ cpgConfig.debugParser = true
+ cpgConfig.failOnError = false
+ cpgConfig.defaultPasses = true
+ cpgConfig.passes = listOf(IdentifierPass(), EdgeCachePass())
+ return Configuration(codyzeConfig, cpgConfig)
+ }
}
}

diff:
+package de.fraunhofer.aisec.codyze.legacy.crymlin
+
+import de.fraunhofer.aisec.codyze.legacy.analysis.AnalysisContext
+import de.fraunhofer.aisec.codyze.legacy.analysis.AnalysisServer
+import de.fraunhofer.aisec.codyze.legacy.config.CodyzeConfiguration
+import java.io.*
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.jupiter.api.*
+
+internal class AnalysisServerWithFileTest : AbstractTest() {
+ @Test
+ fun markModelTest() {
+ val markModel = server.markModel
+ assertNotNull(markModel)
+
+ val rules = markModel.rules
+ assertEquals(7, rules.size)
+
+ val entities = markModel.entities
+ assertEquals(9, entities.size)
+ }
+
+ @Test
+ fun markEvaluationTest() {
+ val ctx = result
+ assertNotNull(ctx)
+
+ val findings: MutableList<String> = ArrayList()
+ assertNotNull(ctx.findings)
+
+ ctx.findings.forEach(Consumer { findings.add(it.toString()) })
+
+ println("Findings")
+ for (finding in findings) {
+ println(finding)
+ }
+ }
+
+ companion object {
+ private lateinit var server: AnalysisServer
+ private var result: AnalysisContext? = null
+ @BeforeAll
+ @Throws(Exception::class)
+ @JvmStatic
+ fun startup() {
+ val classLoader = AnalysisServerBotanTest::class.java.classLoader
+ var resource = classLoader.getResource("legacy/mark_cpp/symm_block_cipher.cpp")
+ assertNotNull(resource)
+ val cppFile = File(resource.file)
+ assertNotNull(cppFile)
+ resource = classLoader.getResource("legacy/mark/PoC_MS1/Botan_AutoSeededRNG.mark")
+ assertNotNull(resource)
+ val markPoC1 = File(resource.file)
+ assertNotNull(markPoC1)
+ val markModelFiles = markPoC1.parent
+
+ // Start an analysis server
+ val codyze = CodyzeConfiguration()
+ codyze.mark = arrayOf(File(markModelFiles))
+
+ val config = newAnalysisRun(codyze)
+ config.executionMode.isCli = false
+ config.executionMode.isLsp = false
+
+ server = AnalysisServer(config)
+ server.start()
+
+ // Start the analysis (BOTAN Symmetric Example by Oliver)
+ val analyze = server.analyze(cppFile.absolutePath)
+ try {
+ result = analyze[5, TimeUnit.MINUTES]
+ } catch (t: TimeoutException) {
+ analyze.cancel(true)
+ throw t
+ }
+ }
+
+ @AfterAll
+ fun teardown() {
+ // Stop the analysis server
+ server.stop()
+ }
+ }
+}

diff:
+package de.fraunhofer.aisec.codyze.legacy.crymlin
+
+import de.fraunhofer.aisec.codyze.legacy.analysis.markevaluation.followNextDFG
+import de.fraunhofer.aisec.cpg.graph.Node
+import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation
+import de.fraunhofer.aisec.cpg.sarif.Region
+import java.net.URI
+import kotlin.test.*
+import org.junit.jupiter.api.Test
+
+class EvaluationHelperTest {
+ @Test
+ fun followNextDFGTest() {
+ val node = Node()
+ node.id = 0
+ node.location = PhysicalLocation(URI("0"), Region(0, 0, 0, 0))
+
+ val expectedResult = mutableListOf<Node>()
+ var current = node
+ for (i in 1..4) {
+ val next = Node()
+ next.id = i.toLong()
+ next.location = PhysicalLocation(URI("$i"), Region(i, i, i, i))
+ if (i <= 3) expectedResult.add(next)
+
+ current.addNextDFG(next)
+ current = next
+ }
+
+ val result = node.followNextDFG { n -> n.id == 3.toLong() }
+ assertNotNull(result)
+ assertEquals(expectedResult.size, result.size, "Size was not as expected")
+ for (n in expectedResult) {
+ assertContains(result, n, "Did not contain node $n")
+ }
+ }
+}

diff:
package de.fraunhofer.aisec.codyze.legacy.crymlin
import de.fraunhofer.aisec.codyze.legacy.analysis.markevaluation.ExpressionHelper
+import de.fraunhofer.aisec.mark.markDsl.impl.*
+import kotlin.test.assertContains
import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import org.junit.jupiter.api.*
internal class ExpressionHelperTest {
@@ -28,11 +32,96 @@ internal class ExpressionHelperTest {
)
}
+ @Test
+ fun testComparableFloat() {
+ assertEquals("0.0", ExpressionHelper.toComparableString(0.toFloat()))
+ assertEquals("0.0", ExpressionHelper.toComparableString(0.0f))
+ assertEquals("1.2", ExpressionHelper.toComparableString(1.2f))
+ assertEquals("-1.2", ExpressionHelper.toComparableString(-1.2f))
+ assertEquals("1.4E-45", ExpressionHelper.toComparableString(Float.MIN_VALUE))
+ assertEquals("3.4028235E38", ExpressionHelper.toComparableString(Float.MAX_VALUE))
+ }
+
@Test
fun testComparableString() {
assertEquals("", ExpressionHelper.toComparableString(""))
assertEquals("a", ExpressionHelper.toComparableString("a"))
assertEquals("1.0", ExpressionHelper.toComparableString("1"))
assertEquals("-1.0", ExpressionHelper.toComparableString("-1"))
+ assertEquals("a", ExpressionHelper.toComparableString("\"a"))
+
+ assertEquals("ab", ExpressionHelper.toComparableString("ab\""))
+ assertEquals("abc", ExpressionHelper.toComparableString("\"abc\""))
+
+ assertEquals("4.0", ExpressionHelper.toComparableString(4.toByte()))
+ assertEquals("a", ExpressionHelper.toComparableString('a'))
+ }
+
+ @Test
+ fun testComparableStringObject() {
+ // uses String implementation -> NullPointerException
+ // assertEquals("", ExpressionHelper.toComparableString(null))
+
+ val nullObject: kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType.Object? = null
+ assertEquals("", ExpressionHelper.toComparableString(nullObject))
+ }
+
+ @Test
+ fun testCollectVars() {
+ val vars = mutableSetOf<String>()
+ val expr =
+ funcCall(
+ "function",
+ logicalAnd(
+ left =
+ comparison(
+ left = mul(left = operand("b"), op = "*", right = lit(1)),
+ op = "==",
+ right = operand("a")
+ ),
+ op = "&&",
+ right =
+ logicalOr(
+ left =
+ comparison(
+ left = operand("a"),
+ op = "==",
+ right = unary(exp = operand("c"), op = "-")
+ ),
+ op = "||",
+ right = comparison(left = lit(1), op = "==", right = lit(1))
+ )
+ ),
+ litList(lit(1), lit("s"), lit("1"))
+ )
+ assertNotNull(expr)
+
+ val expectedVars = setOf("a", "b", "c")
+
+ ExpressionHelper.collectVars(expr, vars)
+ assertEquals(expectedVars.size, vars.size, "Size of vars set was not equal")
+ for (variable in expectedVars) {
+ assertContains(vars, variable, "\"$variable\" was not in vars set")
+ }
+ }
+
+ @Test
+ fun testAsString() {
+ assertNull(ExpressionHelper.asString(null))
+ assertEquals("myString", ExpressionHelper.asString("myString"))
+ }
+
+ @Test
+ fun testAsNumber() {
+ assertNull(ExpressionHelper.asNumber(null))
+ assertEquals(1, ExpressionHelper.asNumber(1))
+ assertNull(ExpressionHelper.asNumber("myString"))
+ }
+
+ @Test
+ fun testAsBoolean() {
+ assertNull(ExpressionHelper.asBoolean(null))
+ assertEquals(true, ExpressionHelper.asBoolean(true))
+ assertNull(ExpressionHelper.asBoolean("true"))
}
}

diff:
+package de.fraunhofer.aisec.codyze.legacy.crymlin
+
+import de.fraunhofer.aisec.codyze.legacy.analysis.utils.Utils
+import de.fraunhofer.aisec.cpg.graph.Node
+import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation
+import de.fraunhofer.aisec.cpg.sarif.Region
+import java.net.URI
+import kotlin.test.*
+import org.junit.jupiter.api.Test
+
+internal class UtilsTest {
+ @Test
+ fun getRegionTest() {
+ val node = Node()
+ val resultNoLocation = Utils.getRegion(node)
+ assertEquals(-1, resultNoLocation.endColumn, "endColumn was not equal")
+ assertEquals(-1, resultNoLocation.startColumn, "startColumn was not equal")
+ assertEquals(-1, resultNoLocation.endLine, "endLine was not equal")
+ assertEquals(-1, resultNoLocation.startLine, "startLine was not equal")
+
+ val region = Region(0, 0, 0, 0)
+ node.location = PhysicalLocation(URI("0"), region)
+ val resultWithLocation = Utils.getRegion(node)
+ assertEquals(0, resultWithLocation.endColumn, "endColumn was not equal")
+ assertEquals(0, resultWithLocation.startColumn, "startColumn was not equal")
+ assertEquals(0, resultWithLocation.endLine, "endLine was not equal")
+ assertEquals(0, resultWithLocation.startLine, "startLine was not equal")
+ }
+
+ @Test
+ fun stripQuotedCharacterTest() {
+ assertEquals("s", Utils.stripQuotedCharacter("\'s\'"))
+ assertEquals("s\'", Utils.stripQuotedCharacter("s\'"))
+ assertEquals("\'s", Utils.stripQuotedCharacter("\'s"))
+ assertEquals("s", Utils.stripQuotedCharacter("s"))
+ }
+
+ @Test
+ fun extractMethodNameTest() {
+ assertEquals("cppMethod", Utils.extractMethodName("CppClass::cppMethod"))
+ assertEquals("method", Utils.extractMethodName("class->method"))
+ assertEquals("javaMethod", Utils.extractMethodName("package.JavaClass.javaMethod"))
+ assertEquals("typo:method", Utils.extractMethodName("typo:method"))
+ }
+}

diff:
@@ -2,16 +2,42 @@ package de.fraunhofer.aisec.mark.markDsl.impl
import de.fraunhofer.aisec.mark.markDsl.*
-fun EnsureStatement.comparison(
- left: Expression,
- right: Expression,
- op: String
-): ComparisonExpression {
+fun comparison(left: Expression, right: Expression, op: String): ComparisonExpression {
val expression = ComparisonExpressionImpl()
expression.left = left
expression.right = right
expression.op = op
- this.exp = expression
+ return expression
+}
+
+fun mul(left: Expression, right: Expression, op: String): MultiplicationExpression {
+ val expression = MultiplicationExpressionImpl()
+ expression.left = left
+ expression.right = right
+ expression.op = op
+ return expression
+}
+
+fun unary(exp: Expression, op: String): UnaryExpression {
+ val expression = UnaryExpressionImpl()
+ expression.exp = exp
+ expression.op = op
+ return expression
+}
+
+fun logicalAnd(left: Expression, right: Expression, op: String): LogicalAndExpression {
+ val expression = LogicalAndExpressionImpl()
+ expression.left = left
+ expression.right = right
+ expression.op = op
+ return expression
+}
+
+fun logicalOr(left: Expression, right: Expression, op: String): LogicalOrExpression {
+ val expression = LogicalOrExpressionImpl()
+ expression.left = left
+ expression.right = right
+ expression.op = op
return expression
}
@@ -29,6 +55,25 @@ fun lit(value: String): Literal {
fun lit(value: Int): Literal {
val expression = IntegerLiteralImpl()
+ expression.value = "0x${Integer.toHexString(value)}"
+ return expression
+}
+
+fun lit(value: Boolean): Literal {
+ val expression = BooleanLiteralImpl()
expression.value = value.toString()
return expression
}
+
+fun litList(vararg values: Literal): LiteralListExpression {
+ val expression = LiteralListExpressionImpl()
+ expression.getValues().addAll(values)
+ return expression
+}
+
+fun funcCall(name: String, vararg args: Argument): FunctionCallExpression {
+ val expression = FunctionCallExpressionImpl()
+ expression.getArgs().addAll(args)
+ expression.name = name
+ return expression
+}

original_message:
Add miscellaneous tests (#471)

This PR adds some more miscellaneous tests for more code coverage.

Co-authored-by: Wendland, Florian <florian.wendland@aisec.fraunhofer.de>
Co-authored-by: Maximilian Kaul <maximilian.kaul@aisec.fraunhofer.de>


message: Fix broken wiki link in README

change_type: MODIFY

diff:
@@ -46,7 +46,7 @@ $ ./bin/codyze -m ./mark -s <sourcepath>
where `<sourcepath>` denotes the path to the source directory or file which should be analyzed.
Codyze can be further configured with additional command line arguments or a YAML configuration file.
-For more information about the usage and configurations, please refer to https://www.codyze.io and the corresponding [wiki page](https://github.com/Fraunhofer-AISEC/codyze/wiki/Usage).
+For more information about the usage and configurations, please refer to https://www.codyze.io and the corresponding [wiki page](https://github.com/Fraunhofer-AISEC/codyze/wiki/Configuring-Codyze).
## Research & Student Work

license: Apache License 2.0

repo: fraunhofer-aisec/codyze

original_message: Fix broken wiki link in README (#482)


message: Fix ConfigLoadTest

diff:
@@ -11,6 +11,7 @@ import kotlin.Throws
import kotlin.test.*
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
internal class ConfigLoadTest {
@@ -178,21 +179,50 @@ internal class ConfigLoadTest {
var config = Configuration.initConfig(paths1File, "-c")
assertNotNull(config.source)
assertContentEquals(
- listOf(File("/absolute/path/to/source").absolutePath),
+ listOf(
+ File(File.separator)
+ .resolve("absolute")
+ .resolve("path")
+ .resolve("to")
+ .resolve("source")
+ .absolutePath
+ ),
config.source.map { f -> f.absolutePath }
)
assertEquals(
- File(paths1File.absoluteFile.parent, "../relative/path/to/output").absolutePath,
+ paths1File.absoluteFile.parentFile
+ .resolve("..")
+ .resolve("relative")
+ .resolve("path")
+ .resolve("to")
+ .resolve("output")
+ .absolutePath,
config.output
)
config = Configuration.initConfig(paths2File, "-c")
assertNotNull(config.source)
assertContentEquals(
- listOf(File(paths2File.absoluteFile.parent, "../relative/path/to/source").absolutePath),
+ listOf(
+ paths2File.absoluteFile.parentFile
+ .resolve("..")
+ .resolve("relative")
+ .resolve("path")
+ .resolve("to")
+ .resolve("source")
+ .absolutePath
+ ),
config.source.map { f -> f.absolutePath }
)
- assertEquals("/absolute/path/to/output", config.output)
+ assertEquals(
+ File(File.separator)
+ .resolve("absolute")
+ .resolve("path")
+ .resolve("to")
+ .resolve("output")
+ .absolutePath,
+ config.output
+ )
}
companion object {
@@ -203,6 +233,8 @@ internal class ConfigLoadTest {
private lateinit var paths1File: File
private lateinit var paths2File: File
+ @TempDir @JvmStatic lateinit var tempDir: File
+
@BeforeAll
@JvmStatic
fun startup() {
@@ -242,17 +274,66 @@ internal class ConfigLoadTest {
unknownLanguageFile = File(unknownLanguageResource.file)
assertNotNull(unknownLanguageFile)
- val paths1Resource =
- ConfigLoadTest::class.java.classLoader.getResource("config-files/paths1.yml")
- assertNotNull(paths1Resource)
- paths1File = File(paths1Resource.file)
- assertNotNull(paths1File)
-
- val paths2Resource =
- ConfigLoadTest::class.java.classLoader.getResource("config-files/paths2.yml")
- assertNotNull(paths2Resource)
- paths2File = File(paths2Resource.file)
- assertNotNull(paths2File)
+ // Build paths test files dynamically because different separators on different OS
+ assertNotNull(tempDir)
+ assertTrue(tempDir.isDirectory)
+
+ paths1File = tempDir.resolve("paths1.yml")
+ paths2File = tempDir.resolve("paths2.yml")
+
+ val sb1 = StringBuilder()
+ sb1.append(
+ """
+ source:
+ -
+ """.trimIndent()
+ )
+ .append(
+ File(File.separator)
+ .resolve("absolute")
+ .resolve("path")
+ .resolve("to")
+ .resolve("source")
+ .absolutePath
+ )
+ .append("\n")
+ .append("output: ")
+ .append(
+ File("..")
+ .resolve("relative")
+ .resolve("path")
+ .resolve("to")
+ .resolve("output")
+ .toString()
+ )
+ paths1File.writeText(sb1.toString())
+
+ val sb2 = StringBuilder()
+ sb2.append(
+ """
+ source:
+ -
+ """.trimIndent()
+ )
+ .append(
+ File("..")
+ .resolve("relative")
+ .resolve("path")
+ .resolve("to")
+ .resolve("source")
+ .toString()
+ )
+ .append("\n")
+ .append("output: ")
+ .append(
+ File(File.separator)
+ .resolve("absolute")
+ .resolve("path")
+ .resolve("to")
+ .resolve("output")
+ .absolutePath
+ )
+ paths2File.writeText(sb2.toString())
}
}
}

original_message:
Fix ConfigLoadTest (#557)

Co-authored-by: Florian Wendland <florian.wendland@aisec.fraunhofer.de>


message: Add Version Provider for v3

diff:
allprojects {
group = "de.fraunhofer.aisec.codyze"
- version = System.getProperty("version") ?: "0.0.0-SNAPSHOT"
+ version = if(version != Project.DEFAULT_VERSION) version else "0.0.0-SNAPSHOT"
}
plugins {

diff:
@@ -10,10 +10,13 @@ plugins {
id("com.diffplug.spotless")
}
-// bundle codyze's version for all jars to use it at runtime
+// configure shared attributes for JAR file manifests
tasks.withType<Jar>().configureEach {
manifest {
- attributes(mapOf("CodyzeVersion" to project.version))
+ attributes(
+ "Implementation-Version" to project.version,
+ "Implementation-Vendor" to "Fraunhofer AISEC"
+ )
}
}

diff:
@@ -17,3 +17,11 @@ dependencies {
// The code in it was generated using https://app.quicktype.io/ with minor manual additions
implementation(libs.sarif4k)
}
+
+tasks {
+ jar {
+ manifest {
+ attributes("Implementation-Title" to "Codyze v3 - Core Library")
+ }
+ }
+}
\ No newline at end of file

diff:
@@ -27,3 +27,11 @@ dependencies {
// LSP interface support
//api("org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0") // ok
}
+
+tasks {
+ jar {
+ manifest {
+ attributes("Implementation-Title" to "Codyze v3 - Specification Language MARK")
+ }
+ }
+}

diff:
@@ -33,3 +33,20 @@ dependencies {
application {
mainClass.set("de.fraunhofer.aisec.codyze.MainKt")
}
+
+tasks {
+ jar {
+ manifest {
+ attributes("Implementation-Title" to "Codyze v3")
+ }
+ }
+}
+
+/**
+ * Set version as system property when using run task, because there isn't a JAR file manifest.
+ *
+ * We already defined a sensible version in our parent project for all modules.
+ */
+tasks.named<JavaExec>("run") {
+ systemProperty("codyze-v3-version", project.version)
+}
\ No newline at end of file

diff:
@@ -9,7 +9,12 @@ import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.*
import de.fraunhofer.aisec.codyze.options.configFileOption
import de.fraunhofer.aisec.codyze.source.JsonValueSource
+import java.io.IOException
+import java.net.URL
import java.nio.file.Path
+import java.util.*
+import java.util.jar.Attributes
+import java.util.jar.Manifest
import kotlin.io.path.Path
import mu.KotlinLogging
import org.koin.core.context.startKoin
@@ -43,7 +48,7 @@ class ConfigFileParser : CliktCommand(treatUnknownOptionsAsArgs = true) {
class CodyzeCli(val configFile: Path? = null) :
CliktCommand(help = "Codyze finds security flaws in source code", printHelpOnEmptyArgs = true) {
init {
- versionOption("1.0", names = setOf("--version", "-V")) // TODO get actual version
+ versionOption(Codyze.version, names = setOf("--version", "-V"))
context {
if (configFile != null)
valueSource = JsonValueSource.from(configFile, requireValid = true)
@@ -51,6 +56,53 @@ class CodyzeCli(val configFile: Path? = null) :
}
}
+ /**
+ * Storage for constant application information.
+ *
+ * Note: Using `object` to keep information as singleton and prevent initialization on each
+ * object creation of [CodyzeCli].
+ */
+ private object Codyze {
+ /** Name of our application. */
+ private const val IMPLEMENTATION_TITLE: String = "Codyze v3"
+
+ /** Default version to use, when no explicit version has been set. */
+ private const val DEFAULT_VERSION: String = "0.0.0-SNAPSHOT"
+
+ /**
+ * Determine version from system property `codyze-v3-version` or from Codyzes' JAR file
+ * manifests.
+ *
+ * Note: Using `lazy` delegate to calculate property once, when it is needed. Reduces
+ * overhead by not reading in all manifests on the classpath multiple times.
+ */
+ val version: String by lazy {
+ System.getProperty("codyze-v3-version")
+ ?: run {
+ val resources: Enumeration<URL> =
+ CodyzeCli::class.java.classLoader.getResources("META-INF/MANIFEST.MF")
+ while (resources.hasMoreElements()) {
+ val url = resources.nextElement()
+ try {
+ val manifest = Manifest(url.openStream())
+ val mainAttributes = manifest.mainAttributes
+ if (
+ IMPLEMENTATION_TITLE ==
+ mainAttributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE)
+ ) {
+ return@run mainAttributes.getValue(
+ Attributes.Name.IMPLEMENTATION_VERSION
+ )
+ }
+ } catch (ex: IOException) {
+ logger.trace { "Unable to read from $url: $ex" }
+ }
+ }
+ DEFAULT_VERSION
+ }
+ }
+ }
+
override fun run() {
echo("In CodyzeCli")
} // TODO: change to NoOpCliktCommand?

original_message:
Add Version Provider for v3 (#570)

Co-authored-by: Florian Wendland <florian.wendland@aisec.fraunhofer.de>


diff:
Codyze is a static analysis tool to support developers in verifying compliance to security requirements.

-
Head to the release page and download Codyze.

- We offer IDE plugins for Eclipse,
- Visual Studio and
- Visual Studio Code.
- Moreover, we support LSP for integration into your favorite IDE or text editor.
+
Head to the release page and download Codyze.

+ We offer IDE plugins for Eclipse,
+ Visual Studio and
+ Visual Studio Code.
+ Moreover, we support LSP for integration into your favorite IDE or text editor.

Develop and Contribute

We are happy to accept pull requests for bugfixes and improvements on GitHub!

-
If you are interested to write a bachelor/master/PhD thesis in static analysis, we encourage you to get in contact with us. Besides the open research topics offered by the Chair of IT Security at the Technical University of Munich, we are open to discuss further topics which might be of interest for you and us.

+
If you are interested to write a bachelor/master/PhD thesis in static analysis, we encourage you to get in contact with us. Besides the open research topics offered by the Chair of IT Security at the Technical University of Munich, we are open to discuss further topics which might be of interest for you and us.

original_message:
fix external links so that the font of the link text is not changed (#587)

Co-authored-by: Florian Wendland <florian.wendland@aisec.fraunhofer.de>


diff:
@@ -2,20 +2,23 @@ package de.fraunhofer.aisec.codyze_core
import de.fraunhofer.aisec.codyze_core.config.Configuration
import io.github.detekt.sarif4k.*
+import org.koin.core.component.KoinComponent
/**
* An object that saves the context of an analysis.
*
* This enables switching between different analyses (e.g. switching between projects in an IDE).
*/
-class Project(val config: Configuration) {
+class Project(val config: Configuration) : KoinComponent {
/** [Executor] that is capable of evaluating the [Configuration.spec] given in [config] */
val executor = config.executor ?: getRandomCapableExecutor()
/** Return the first registered Executor capable of evaluating [config.specFileExtension] */
private fun getRandomCapableExecutor(): Executor {
val randomCapableExecutor: Executor? =
- ProjectServer.executors.find { it.supportedFileExtension == config.specFileExtension }
+ getKoin().getAll<Executor>().find {
+ it.supportedFileExtension == config.specFileExtension
+ }
if (randomCapableExecutor != null) return randomCapableExecutor
else
throw RuntimeException(

diff:
@@ -2,25 +2,16 @@ package de.fraunhofer.aisec.codyze_core
import de.fraunhofer.aisec.codyze_core.config.Configuration
import mu.KotlinLogging
-import org.koin.core.component.KoinComponent
private val logger = KotlinLogging.logger {}
/**
* A server that manages [Project]s. Each [Configuration] corresponds to one potential [Project] .
*/
-object ProjectServer : KoinComponent {
+object ProjectServer {
/** All projects that are connected to the server. */
val projects: MutableMap<Configuration, Project> = mutableMapOf()
- /** All built-in executors that are available for the analysis. */
- val executors: List<Executor> = getKoin().getAll()
-
- init {
- logger.debug {
- "Found executors for following file types: ${executors.map { it.supportedFileExtension }}"
- }
- }
/**
* Connect to the project associated with the given [Configuration] object.

diff:
@@ -6,16 +6,17 @@ import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.path
import de.fraunhofer.aisec.codyze_core.Executor
-import de.fraunhofer.aisec.codyze_core.ProjectServer
import de.fraunhofer.aisec.codyze_core.config.ConfigurationRegister
import de.fraunhofer.aisec.codyze_core.config.combineSources
import de.fraunhofer.aisec.codyze_core.config.validateSpec
+import de.fraunhofer.aisec.codyze_core.log
import java.nio.file.Path
import kotlin.io.path.Path
+import org.koin.core.component.KoinComponent
@Suppress("UNUSED")
class CodyzeOptionGroup(configurationRegister: ConfigurationRegister) :
- OptionGroup(name = "Codyze Options") {
+ OptionGroup(name = "Codyze Options"), KoinComponent {
private val rawSpec: List<Path> by
option("--spec", help = "Loads the given specification files.")
.path(mustExist = true, mustBeReadable = true, canBeDir = true)
@@ -89,11 +90,19 @@ class CodyzeOptionGroup(configurationRegister: ConfigurationRegister) :
help =
"Manually choose Executor to use with the given spec files. If unspecified, Codyze randomly selects an executor capable of evaluating the given specification files."
)
- .choice(*(ProjectServer.executors.map { it.name }).toTypedArray(), ignoreCase = true)
+ .choice(
+ *(getKoin().getAll<Executor>().map { it.name }).toTypedArray(),
+ ignoreCase = true
+ )
.convert {
- it.let { ProjectServer.executors.first { executor -> executor.name == it } }
+ it.let { getKoin().getAll<Executor>().first { executor -> executor.name == it } }
+ }
+ .also {
+ configurationRegister.addOption("executor", it)
+ log.debug {
+ "Found following executors: ${it.completionCandidates}"
+ }
}
- .also { configurationRegister.addOption("executor", it) }
val output: Path by
option("-o", "--output", help = "Write results to file. Use - for stdout.")

original_message: fix `executorOptionCastTest` by getting them from Koin instead of the `ProjectServer` (#659)


message:
More Saha names
Ruble suggested "Salamander" and it does in fact fit the 10-character requirement.

diff:
@@ -627,7 +627,7 @@ class Randomizer {
"saccharine", "saccharins", "sacerdotal", "sackcloths", "salmonella", "saltarelli", "saltarello",
"saltations", "saltbushes", "saltcellar", "saltshaker", "salubrious", "sandgrouse", "sandlotter",
"sandstorms", "sandwiched", "sauerkraut", "schipperke", "schismatic", "schizocarp", "schmalzier",
- "schmeering", "schmoosing", "shibboleth", "shovelnose", "sahananana", "sarararara",
+ "schmeering", "schmoosing", "shibboleth", "shovelnose", "sahananana", "sarararara", "salamander",
]));
$rom->setKakarikoTownCredits("$name's homecoming");

license: MIT License


message:
Remove empty div on Custom Options page
Remove empty <div> on Custom Options page that was causing some odd layout behavior.

diff:
<input id="cust-region-CompassesMaps" type="checkbox" name="data[alttp.custom.region.CompassesMaps]" value="true" checked data-toggle="toggle" data-on="Yes" data-off="No" data-size="small">
Dungeons Contain Compasses and Maps

-

-

<input id="cust-region-bonkItems" type="checkbox" name="data[alttp.custom.region.bonkItems]" value="true" checked data-toggle="toggle" data-on="Yes" data-off="No" data-size="small">
Bonk Keys in Pool


message:
Goals display
Goals were added but not exposed everywhere.

diff:
@@ -182,10 +182,12 @@ function seedApplied(data) {
$('.info .seed').html(data.patch.hash);
$('.info .logic').html(data.patch.spoiler.meta.logic);
$('.info .build').html(data.patch.spoiler.meta.build);
+ $('.info .goal').html(data.patch.spoiler.meta.goal);
$('.info .mode').html(data.patch.spoiler.meta.mode);
$('.info .difficulty').html(data.patch.difficulty);
rom.logic = data.patch.spoiler.meta.logic;
rom.build = data.patch.spoiler.meta.build;
+ rom.goal = data.patch.spoiler.meta.goal;
rom.mode = data.patch.spoiler.meta.mode;
rom.difficulty = data.patch.difficulty;
rom.seed = data.patch.hash;
@@ -223,7 +225,7 @@ $(function() {
$('button[name=save]').hide();
$('button[name=save]').on('click', function() {
- return rom.save('ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '_' + rom.seed + '.sfc');
+ return rom.save('ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '-' + rom.goal + '_' + rom.seed + '.sfc');
});
$('input[name=f2u]').on('change', function() {


message: Update randomizer.blade.php

diff:
@@ -756,18 +756,18 @@ $(function() {
});
$('button[name=save]').on('click', function() {
- return rom.save('ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '_' + rom.seed + '.sfc');
+ return rom.save('ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '-' + rom.goal + '_' + rom.seed + '.sfc');
});
$('button[name=save-spoiler]').on('click', function() {
$.get("/spoiler_click/" + rom.seed);
- return saveAs(new Blob([$('.spoiler-text pre').html()]), 'ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '_' + rom.seed + '.txt');
+ return saveAs(new Blob([$('.spoiler-text pre').html()]), 'ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '-' + rom.goal + '_' + rom.seed + '.txt');
});
$('button[name=generate-save]').on('click', function() {
applySeed(rom, $('#seed').val())
.then(seedApplied, seedFailed)
.then(function(rom) {
- return rom.save('ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '_' + rom.seed + '.sfc');
+ return rom.save('ALttP - VT_' + rom.logic + '_' + rom.difficulty + '-' + rom.mode + '-' + rom.goal + '_' + rom.seed + '.sfc');
});
});
@@ -990,8 +990,8 @@ $(function() {
return new Promise(function(resolve, reject) {
applySeed(rom, $('#seed').val()).then(function(data) {
var buffer = data.rom.getArrayBuffer().slice(0);
- zip.file('ALttP - VT_' + data.patch.logic + '_' + data.patch.difficulty + '-' + data.patch.spoiler.meta.mode + '_' + data.patch.seed + '.sfc', buffer);
- zip.file('spoilers/ALttP - VT_' + data.patch.logic + '_' + data.patch.difficulty + '-' + data.patch.spoiler.meta.mode + '_' + data.patch.seed + '.txt', new Blob([JSON.stringify(data.patch.spoiler, null, 4)]));
+ zip.file('ALttP - VT_' + data.patch.logic + '_' + data.patch.difficulty + '-' + data.patch.spoiler.meta.mode + '-' + data.patch.spoiler.meta.goal + '_' + data.patch.seed + '.sfc', buffer);
+ zip.file('spoilers/ALttP - VT_' + data.patch.logic + '_' + data.patch.difficulty + '-' + data.patch.spoiler.meta.mode + '-' + data.patch.spoiler.meta.goal + '_' + data.patch.seed + '.txt', new Blob([JSON.stringify(data.patch.spoiler, null, 4)]));
if (left - 1 > 0) {
genToZip(zip, left - 1).then(function() {
resolve(zip);

original_message: Update randomizer.blade.php


message: Update patch_from_hash.blade.php

diff:
ROM build:

Difficulty:

Mode:

+
Goal:

original_message: Update patch_from_hash.blade.php


message:
Fix typo in function name
* Fix typo in function name
* Fix typo in function name
Also, sneak in some more Sriracha names

diff:
@@ -506,7 +506,7 @@ class Randomizer {
// testing features
$rom->setGanonAgahnimRng($this->config('rom.GanonAgRNG', 'table'));
$rom->setLockAgahnimDoorInEscape(false);
- $rom->setGanonInvincble($this->goal == 'pedestal');
+ $rom->setGanonInvincible($this->goal == 'pedestal');
$rom->writeRNGBlock(function() {
return mt_rand(0, 0x100);
@@ -636,6 +636,7 @@ class Randomizer {
"saltations", "saltbushes", "saltcellar", "saltshaker", "salubrious", "sandgrouse", "sandlotter",
"sandstorms", "sandwiched", "sauerkraut", "schipperke", "schismatic", "schizocarp", "schmalzier",
"schmeering", "schmoosing", "shibboleth", "shovelnose", "sahananana", "sarararara", "salamander",
+ "sharshalah", "shahabadoo",
]));
$rom->setKakarikoTownCredits("$name's homecoming");

diff:
@@ -336,7 +336,7 @@ class Rom {
*
* @return $this
*/
- public function setGanonInvincble($enable = false) : self {
+ public function setGanonInvincible($enable = false) : self {
$this->write(0x18003E, pack('C*', $enable ? 0x01 : 0x00));
return $this;

original_message:
Fix typo in function name (#87)

* Fix typo in function name

* Fix typo in function name

Also, sneak in some more Sriracha names


message:
Lonk's Boots
We talked briefly about having Uncle tell Lonk that boots are in the house. This, like many other things, will need an update after "The Great Item Location Renaming 2017."

diff:
@@ -712,6 +712,9 @@ class Randomizer {
if ($this->config('spoil.BootsLocation', true) && mt_rand() % 20 == 0 && $boots_location) {
Log::info('Boots revealed');
switch ($boots_location->getName()) {
+ case "[cave-040] Link's House":
+ $rom->setUncleTextString("Lonk! Boots\nare in here!");
+ break;
case "Piece of Heart (Maze Race)":
$rom->setUncleTextString("Boots at race?\nSeed confirmed\nimpossible.");
break;

original_message:
Lonk's Boots (#94)

We talked briefly about having Uncle tell Lonk that boots are in the house. This, like many other things, will need an update after "The Great Item Location Renaming 2017."


message:
Add a Saha name
Kliff mentioned "Sassafrass" and I noticed that it's not in the code as an option. This will add it.

diff:
@@ -679,7 +679,7 @@ class Randomizer {
"saltations", "saltbushes", "saltcellar", "saltshaker", "salubrious", "sandgrouse", "sandlotter",
"sandstorms", "sandwiched", "sauerkraut", "schipperke", "schismatic", "schizocarp", "schmalzier",
"schmeering", "schmoosing", "shibboleth", "shovelnose", "sahananana", "sarararara", "salamander",
- "sharshalah", "shahabadoo",
+ "sharshalah", "shahabadoo", "sassafrass"
]));
$rom->setKakarikoTownCredits("$name's homecoming");


message:
Fix comma
Commas are important.

diff:
@@ -679,7 +679,7 @@ class Randomizer {
"saltations", "saltbushes", "saltcellar", "saltshaker", "salubrious", "sandgrouse", "sandlotter",
"sandstorms", "sandwiched", "sauerkraut", "schipperke", "schismatic", "schizocarp", "schmalzier",
"schmeering", "schmoosing", "shibboleth", "shovelnose", "sahananana", "sarararara", "salamander",
- "sharshalah", "shahabadoo", "sassafrass"
+ "sharshalah", "shahabadoo", "sassafrass",
]));
$rom->setKakarikoTownCredits("$name's homecoming");

original_message:
Fix comma

Commas are important.


message: Remove Standard from Entrance Randomizer

diff:
Mode
<select id="mode" class="form-control selectpicker">
- <option value="standard">Standard</option>
<option value="open">Open</option>
<option value="swordless">Swordless</option>
</select>


message:
Remove Standard hidden form value
Do the thing that Veetorp said.
Read the whole document before thinking you've done it.

diff:
<input type="hidden" name="logic" value="NoMajorGlitches" />
<input type="hidden" name="difficulty" value="normal" />
<input type="hidden" name="variation" value="none" />
- <input type="hidden" name="mode" value="standard" />
<input type="hidden" name="goal" value="ganon" />
<input type="hidden" name="shuffle" value="full" />
<input type="hidden" name="heart_speed" value="half" />


message: Always allow flying in inverted mode with the active flute.

diff:
@@ -397,7 +397,7 @@ class ItemCollection extends Collection {
public function canFly() {
if ($this->world instanceof World\Inverted
&& !($this->has('MoonPearl') && $this->world->getRegion('North West Light World')->canEnter([], $this))) {
- return false;
+ return $this->has('OcarinaActive');
}
return $this->has('OcarinaActive') || $this->has('OcarinaInactive');
}


message: Refactor ocarina requirements.

diff:
@@ -395,11 +395,14 @@ class ItemCollection extends Collection {
* @return bool
*/
public function canFly() {
- if ($this->world instanceof World\Inverted
- && !($this->has('MoonPearl') && $this->world->getRegion('North West Light World')->canEnter([], $this))) {
- return $this->has('OcarinaActive');
+ return $this->has('OcarinaActive') || $this->has('OcarinaInactive') && $this->canActivateOcarina();
}
- return $this->has('OcarinaActive') || $this->has('OcarinaInactive');
+
+ private function canActivateOcarina() {
+ if ($this->world instanceof World\Inverted) {
+ return $this->has('MoonPearl') && $this->world->getRegion('North West Light World')->canEnter([], $this);
+ }
+ return true;
}
/**

original_message: Refactor ocarina requirements.


message: Allow progression in Pinball Room when not forcing key.

diff:
@@ -103,7 +103,7 @@ class SkullWoods extends Region
public function initalize()
{
$this->locations["Skull Woods - Pinball Room"]->setFillRules(function ($item, $locations, $items) {
- return $item == Item::get('KeyD3', $this->world);
+ return !$this->world->config('region.forceSkullWoodsKey', false) || $item == Item::get('KeyD3', $this->world);
});
$this->locations["Skull Woods - Big Chest"]->setRequirements(function ($locations, $items) {


message: Don't overflow silvers for swordless seeds.

diff:
@@ -183,6 +183,7 @@ abstract class World
// In swordless mode silvers are 100% required
if ($this->config('mode.weapons') === 'swordless') {
$this->config['region.requireBetterBow'] = true;
+ $this->config['item.overflow.count.Bow'] = 2;
}
if ($this->config('itemPlacement') === 'basic') {


message: Fix ShopKey check.

diff:
@@ -267,7 +267,7 @@ class ItemCollection extends Collection
// @TODO: this check is expensive, as this function is called A LOT, can we reduce it somehow?
// assuming if there are ShopKey's available then we are in generic key mode, this is a really bad assumption
// but we need to make it until we can rewrite this class
- if (($this->item_counts['ShopKey'] ?? false) && strpos($key, 'Key') === 0) {
+ if (($this->item_counts["ShopKey:$this->checks_for_world"] ?? false) && strpos($key, 'Key') === 0) {
return true;
}

original_message: Fix ShopKey check.


message: Remove 10-15 keys from retro (replace with green 20 rupees).

diff:
@@ -21,5 +21,24 @@ class Retro extends Open
'region.takeAnys' => true,
'region.wildKeys' => true,
]));
+
+ if ($this->config('difficulty') !== 'custom') {
+ switch ($this->config('item.pool')) {
+ case 'hard':
+ case 'expert':
+ case 'crowd_control':
+ $this->config['item.count.KeyD1'] = 0;
+ $this->config['item.count.KeyA2'] = 0;
+ $this->config['item.count.KeyD7'] = 0;
+ $this->config['item.count.KeyD2'] = 0;
+
+ $this->config['item.count.TwentyRupees2'] = 15 + $this->config('item.count.TwentyRupees2', 0);
+ break;
+ case 'normal':
+ $this->config['item.count.KeyD1'] = 0;
+ $this->config['item.count.KeyA2'] = 0;
+ $this->config['item.count.TwentyRupees2'] = 10 + $this->config('item.count.TwentyRupees2', 0);
+ }
+ }
}
}


message: Fix pre_collected_items being collected multiple times

diff:
@@ -79,6 +79,9 @@ class RandomAssumed extends Filler
$found_locations = 0;
$assumed_items = $starting_items->copy();
+ foreach ($this->worlds as $world) {
+ $assumed_items = $assumed_items->merge($world->getPreCollectedItems());
+ }
do {
$current_locations = 0;
foreach ($this->worlds as $world) {

diff:
@@ -37,6 +37,9 @@ class WorldCollection
$found_locations = 0;
$assumed_items = new ItemCollection;
+ foreach ($this->worlds as $world) {
+ $assumed_items = $assumed_items->merge($world->getPreCollectedItems());
+ }
do {
$current_locations = 0;
foreach ($this->worlds as $collect_world) {

diff:
@@ -467,10 +467,9 @@ abstract class World
*/
public function collectOtherItems(ItemCollection $collected): ItemCollection
{
- $my_items = $collected ?? new ItemCollection();
- $found = new ItemCollection($this->pre_collected_items);
- $my_items = $my_items->merge($this->pre_collected_items);
+ $my_items = $collected ?? new ItemCollection($this->pre_collected_items);
$my_items->setChecksForWorld($this->id);
+ $found = new ItemCollection();
$available_locations = $this->getCollectableLocations();
do {


message:
Add tempered sword to progression items if logically required to beat the seed.
This is required for beatable only logic.

diff:
@@ -256,11 +256,16 @@ class Randomizer implements RandomizerContract
}
}
- // put 1 more sword back in
+ // put master sword back in
if (count($nice_items_swords)) {
array_push($advancement_items, array_pop($nice_items_swords));
}
+ // put tempered sword back in if logically required
+ if ($world->config('region.requireBetterSword', false) && count($nice_items_swords)) {
+ array_push($advancement_items, array_pop($nice_items_swords));
+ }
+
if (count($nice_items_swords)) {
if ($world->config('region.takeAnys', false)) {
array_pop($nice_items_swords);


message: Make customizer dungeon item text only apply to dungeon items shuffled outside dungeons.

diff:
@@ -157,6 +157,13 @@ class CustomizerController extends Controller
| ($custom_data['region.wildBigKeys'] << 1)
| $custom_data['region.wildKeys'];
}
+ if ($custom_data['rom.freeItemText']) {
+ $custom_data['rom.freeItemText'] = 0x10
+ | ($custom_data['region.wildBigKeys'] << 3)
+ | ($custom_data['region.wildMaps'] << 2)
+ | ($custom_data['region.wildCompasses'] << 1)
+ | $custom_data['region.wildKeys'];
+ }
$world = World::factory($request->input('mode', 'standard'), array_merge([
'difficulty' => 'custom',

diff:
@@ -240,10 +240,8 @@ class Location
$item = $this->item;
- if (
- $item instanceof Item\Key && $this->region->isRegionItem($item)
- && (!in_array($this->name, ["Secret Passage", "Link's Uncle"]) || $item != Item::get('KeyH2', $this->region->getWorld()))
- ) { // special key-sanity case
+ if ($item instanceof Item\Key && $this->region->isRegionItem($item)
+ && (!in_array($this->name, ["Secret Passage", "Link's Uncle"]) || $item != Item::get('KeyH2', $this->region->getWorld()))) { // special key-sanity case
$item = Item::get('Key', $this->region->getWorld());
}
@@ -251,6 +249,15 @@ class Location
$item = Item::get('BigKey', $this->region->getWorld());
}
+ if ($item instanceof Item\Map && $this->region->isRegionItem($item)
+ && (!in_array($this->name, ["Secret Passage", "Link's Uncle"]) || $item != Item::get('MapH2', $this->region->getWorld()))) { // special key-sanity case
+ $item = Item::get('Map', $this->region->getWorld());
+ }
+
+ if ($item instanceof Item\Compass && $this->region->isRegionItem($item)) {
+ $item = Item::get('Compass', $this->region->getWorld());
+ }
+
if ($this->region->getWorld()->config('rom.genericKeys', false) && $item instanceof Item\Key) {
$item = Item::get('KeyGK', $this->region->getWorld());
}


message: Add Uncle Boots hint to sign east of link's house.

diff:
@@ -871,23 +871,26 @@ class Randomizer implements RandomizerContract
];
});
+ if ($world instanceof World\Standard) {
$boots_location = $world->getLocationsWithItem(Item::get('PegasusBoots', $world))->first();
if ($world->config('spoil.BootsLocation', false) && $boots_location) {
Log::info('Boots revealed');
+ $uncleBootsText = "Lonk! Boots\nare in the\n" . $boots_location->getRegion()->getName();
switch ($boots_location->getName()) {
case "Link's House":
- $world->setText('uncle_leaving_text', "Lonk!\nYou'll never\nfind the boots");
+ $uncleBootsText = "Lonk!\nYou'll never\nfind the boots";
break;
case "Maze Race":
- $world->setText('uncle_leaving_text', "Boots at race?\nSeed confirmed\nimpossible.");
+ $uncleBootsText = "Boots at race?\nSeed confirmed\nimpossible.";
break;
- default:
- $world->setText('uncle_leaving_text', "Lonk! Boots\nare in the\n" . $boots_location->getRegion()->getName());
}
+ $world->setText('uncle_leaving_text', $uncleBootsText);
+ $world->setText('sign_east_of_links_house', $uncleBootsText);
} else {
$world->setText('uncle_leaving_text', array_first(fy_shuffle($strings['uncle'])));
}
+ }
$green_pendant_location = $world->getLocationsWithItem(Item::get('PendantOfCourage', $world))->first();


message: Fix special boots hints

diff:
@@ -878,10 +878,10 @@ class Randomizer implements RandomizerContract
Log::info('Boots revealed');
$uncleBootsText = "Lonk! Boots\nare in the\n" . $boots_location->getRegion()->getName();
switch ($boots_location->getName()) {
- case "Link's House":
+ case "Link's House:" . $world->id:
$uncleBootsText = "Lonk!\nYou'll never\nfind the boots";
break;
- case "Maze Race":
+ case "Maze Race:" . $world->id:
$uncleBootsText = "Boots at race?\nSeed confirmed\nimpossible.";
break;
}

original_message: Fix special boots hints


message:
Add a clearer hint for pre-collected boots.
Move "Ganon offered me the boots" to be used for boots on uncle (assured sword).

diff:
@@ -877,19 +877,21 @@ class Randomizer implements RandomizerContract
if ($world->config('spoil.BootsLocation', false)) {
Log::info('Boots revealed');
if ($world->getPreCollectedItems()->has('PegasusBoots')) {
- $uncleBootsText = "I couldn't\nfind my Boots\ntoday. RIP me.";
+ $uncleBootsText = "Lonk! Boots\nare on\nyour feet.";
} else if (!$boots_location) {
- $uncleBootsText = "Ganon offered\nme the Boots.\nTime to run!";
+ $uncleBootsText = "I couldn't\nfind the Boots\ntoday.\nRIP me.";
} else {
$uncleBootsText = "Lonk! Boots\nare in the\n" . $boots_location->getRegion()->getName();
-
switch ($boots_location->getName()) {
case "Link's House:" . $world->id:
- $uncleBootsText = "Lonk!\nYou'll never\nfind the boots";
+ $uncleBootsText = "Lonk!\nYou'll never\nfind the boots.";
break;
case "Maze Race:" . $world->id:
$uncleBootsText = "Boots at race?\nSeed confirmed\nimpossible.";
break;
+ case "Link's Uncle:" . $world->id:
+ $uncleBootsText = "Ganon offered\nme the Boots.\nTime to run!";
+ break;
}
}
$world->setText('uncle_leaving_text', $uncleBootsText);


message: Add tests for boss shuffle.

diff:
@@ -162,4 +162,184 @@ class RandomizerTest extends TestCase
$this->world->getLocation("Master Sword Pedestal")->getItem(),
]);
}
+
+ public function testSimpleBossShuffle()
+ {
+ $this->world = World::factory('standard', ['difficulty' => 'test_rules', 'enemizer.bossShuffle' => 'simple']);
+ $this->randomizer = new Randomizer([$this->world]);
+
+ $this->randomizer->placeBosses($this->world);
+
+ $bosses = array_count_values ([
+ $this->world->getRegion('Eastern Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Desert Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Tower of Hera')->getBoss('')->getEName(),
+ $this->world->getRegion('Palace of Darkness')->getBoss('')->getEName(),
+ $this->world->getRegion('Swamp Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Skull Woods')->getBoss('')->getEName(),
+ $this->world->getRegion('Thieves Town')->getBoss('')->getEName(),
+ $this->world->getRegion('Ice Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Misery Mire')->getBoss('')->getEName(),
+ $this->world->getRegion('Turtle Rock')->getBoss('')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('bottom')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('middle')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('top')->getEName()]);
+
+ $this->assertEquals([2, 2, 2, 1, 1, 1, 1, 1, 1, 1],
+ [
+ $bosses['Armos'],
+ $bosses['Lanmola'],
+ $bosses['Moldorm'],
+ $bosses['Helmasaur'],
+ $bosses['Arrghus'],
+ $bosses['Mothula'],
+ $bosses['Blind'],
+ $bosses['Kholdstare'],
+ $bosses['Vitreous'],
+ $bosses['Trinexx']
+ ]);
+ }
+
+ public function testFullBossShuffle()
+ {
+ $this->world = World::factory('standard', ['difficulty' => 'test_rules', 'enemizer.bossShuffle' => 'full']);
+ $this->randomizer = new Randomizer([$this->world]);
+
+ $this->randomizer->placeBosses($this->world);
+
+ $bosses = array_count_values ([
+ $this->world->getRegion('Eastern Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Desert Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Tower of Hera')->getBoss('')->getEName(),
+ $this->world->getRegion('Palace of Darkness')->getBoss('')->getEName(),
+ $this->world->getRegion('Swamp Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Skull Woods')->getBoss('')->getEName(),
+ $this->world->getRegion('Thieves Town')->getBoss('')->getEName(),
+ $this->world->getRegion('Ice Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Misery Mire')->getBoss('')->getEName(),
+ $this->world->getRegion('Turtle Rock')->getBoss('')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('bottom')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('middle')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('top')->getEName()]);
+
+ $this->assertGreaterThanOrEqual(1, $bosses['Armos']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Lanmola']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Moldorm']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Helmasaur']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Arrghus']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Mothula']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Blind']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Kholdstare']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Vitreous']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Trinexx']);
+ }
+
+ public function testNoBossShuffle()
+ {
+ $this->world = World::factory('standard', ['difficulty' => 'test_rules', 'enemizer.bossShuffle' => 'none']);
+ $this->randomizer = new Randomizer([$this->world]);
+
+ $this->randomizer->placeBosses($this->world);
+
+ $this->assertEquals([
+ 'Armos',
+ 'Lanmola',
+ 'Moldorm',
+ 'Helmasaur',
+ 'Arrghus',
+ 'Mothula',
+ 'Blind',
+ 'Kholdstare',
+ 'Vitreous',
+ 'Trinexx',
+ 'Armos',
+ 'Lanmola',
+ 'Moldorm'
+ ], [
+ $this->world->getRegion('Eastern Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Desert Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Tower of Hera')->getBoss('')->getEName(),
+ $this->world->getRegion('Palace of Darkness')->getBoss('')->getEName(),
+ $this->world->getRegion('Swamp Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Skull Woods')->getBoss('')->getEName(),
+ $this->world->getRegion('Thieves Town')->getBoss('')->getEName(),
+ $this->world->getRegion('Ice Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Misery Mire')->getBoss('')->getEName(),
+ $this->world->getRegion('Turtle Rock')->getBoss('')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('bottom')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('middle')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('top')->getEName()
+ ]);
+ }
+
+ public function testSwordlessSimpleBossShuffle()
+ {
+ $this->world = World::factory('standard', ['difficulty' => 'test_rules', 'enemizer.bossShuffle' => 'simple', 'mode.weapons' => 'swordless']);
+ $this->randomizer = new Randomizer([$this->world]);
+
+ $this->randomizer->placeBosses($this->world);
+
+ $bosses = array_count_values ([
+ $this->world->getRegion('Eastern Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Desert Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Tower of Hera')->getBoss('')->getEName(),
+ $this->world->getRegion('Palace of Darkness')->getBoss('')->getEName(),
+ $this->world->getRegion('Swamp Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Skull Woods')->getBoss('')->getEName(),
+ $this->world->getRegion('Thieves Town')->getBoss('')->getEName(),
+ $this->world->getRegion('Ice Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Misery Mire')->getBoss('')->getEName(),
+ $this->world->getRegion('Turtle Rock')->getBoss('')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('bottom')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('middle')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('top')->getEName()]);
+
+ $this->assertEquals([2, 2, 2, 1, 1, 1, 1, 1, 1, 1],
+ [
+ $bosses['Armos'],
+ $bosses['Lanmola'],
+ $bosses['Moldorm'],
+ $bosses['Helmasaur'],
+ $bosses['Arrghus'],
+ $bosses['Mothula'],
+ $bosses['Blind'],
+ $bosses['Kholdstare'],
+ $bosses['Vitreous'],
+ $bosses['Trinexx']
+ ]);
+ }
+
+ public function testSwordlessFullBossShuffle()
+ {
+ $this->world = World::factory('standard', ['difficulty' => 'test_rules', 'enemizer.bossShuffle' => 'full', 'mode.weapons' => 'swordless']);
+ $this->randomizer = new Randomizer([$this->world]);
+
+ $this->randomizer->placeBosses($this->world);
+
+ $bosses = array_count_values ([
+ $this->world->getRegion('Eastern Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Desert Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Tower of Hera')->getBoss('')->getEName(),
+ $this->world->getRegion('Palace of Darkness')->getBoss('')->getEName(),
+ $this->world->getRegion('Swamp Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Skull Woods')->getBoss('')->getEName(),
+ $this->world->getRegion('Thieves Town')->getBoss('')->getEName(),
+ $this->world->getRegion('Ice Palace')->getBoss('')->getEName(),
+ $this->world->getRegion('Misery Mire')->getBoss('')->getEName(),
+ $this->world->getRegion('Turtle Rock')->getBoss('')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('bottom')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('middle')->getEName(),
+ $this->world->getRegion('Ganons Tower')->getBoss('top')->getEName()]);
+
+ $this->assertGreaterThanOrEqual(1, $bosses['Armos']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Lanmola']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Moldorm']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Helmasaur']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Arrghus']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Mothula']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Blind']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Kholdstare']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Vitreous']);
+ $this->assertGreaterThanOrEqual(1, $bosses['Trinexx']);
+ }
}

original_message: Add tests for boss shuffle.


message: Allow killing Helma with fighter's sword, for advanced item placement.

diff:
@@ -85,7 +85,8 @@ class Boss
}),
new static("Helmasaur King", "Helmasaur", function ($locations, $items) use ($world) {
return ($items->canBombThings() || $items->has('Hammer'))
- && ($items->hasSword(2) || $items->canShootArrows($world));
+ && ($items->hasSword(2) || $items->canShootArrows($world)
+ || ($world->config('itemPlacement') !== 'basic' && $items->hasSword()));
}),
new static("Arrghus", "Arrghus", function ($locations, $items) use ($world) {
return ($world->config('itemPlacement') !== 'basic' || $world->config('mode.weapons') === 'swordless' || $items->hasSword(2))


message: Fix issue with item menu showing the wrong dungeon items (compass/map indicators are flipped).

diff:
@@ -152,8 +152,8 @@ class CustomizerController extends Controller
$custom_data['item.require.Lamp'] = $custom_data['item.require.Lamp'] ? 0 : 1;
if ($custom_data['rom.freeItemMenu']) {
$custom_data['rom.freeItemMenu'] = 0x00
- | ($custom_data['region.wildMaps'] << 3)
- | ($custom_data['region.wildCompasses'] << 2)
+ | ($custom_data['region.wildCompasses'] << 3)
+ | ($custom_data['region.wildMaps'] << 2)
| ($custom_data['region.wildBigKeys'] << 1)
| $custom_data['region.wildKeys'];
}


message: Fix some copy/paste errors in the Inverted OWG tests.

diff:
@@ -92,9 +92,9 @@ class EasternPalaceTest extends TestCase
["Eastern Palace - Compass Chest", true, ['DefeatAgahnim']],
["Eastern Palace - Cannonball Chest", false, []],
- ["Eastern Palace - Compass Chest", true, ['MoonPearl', 'PegasusBoots']],
- ["Eastern Palace - Compass Chest", true, ['MagicMirror', 'PegasusBoots']],
- ["Eastern Palace - Compass Chest", true, ['DefeatAgahnim']],
+ ["Eastern Palace - Cannonball Chest", true, ['MoonPearl', 'PegasusBoots']],
+ ["Eastern Palace - Cannonball Chest", true, ['MagicMirror', 'PegasusBoots']],
+ ["Eastern Palace - Cannonball Chest", true, ['DefeatAgahnim']],
["Eastern Palace - Big Chest", false, []],
["Eastern Palace - Big Chest", false, [], ['BigKeyP1']],
@@ -103,9 +103,9 @@ class EasternPalaceTest extends TestCase
["Eastern Palace - Big Chest", true, ['BigKeyP1', 'DefeatAgahnim']],
["Eastern Palace - Map Chest", false, []],
- ["Eastern Palace - Compass Chest", true, ['MoonPearl', 'PegasusBoots']],
- ["Eastern Palace - Compass Chest", true, ['MagicMirror', 'PegasusBoots']],
- ["Eastern Palace - Compass Chest", true, ['DefeatAgahnim']],
+ ["Eastern Palace - Map Chest", true, ['MoonPearl', 'PegasusBoots']],
+ ["Eastern Palace - Map Chest", true, ['MagicMirror', 'PegasusBoots']],
+ ["Eastern Palace - Map Chest", true, ['DefeatAgahnim']],
["Eastern Palace - Big Key Chest", false, []],
["Eastern Palace - Big Key Chest", false, [], ['Lamp']],


message: Fix swordless boss placement to place Kholdstare in Ice Palace.

diff:
@@ -370,7 +370,7 @@ class Randomizer implements RandomizerContract
];
if ($world->config('mode.weapons') == 'swordless') {
- array_splice($boss_locations, 8, 1); // remove Ice Palace
+ array_splice($boss_locations, 9, 1); // remove Ice Palace
$world->getRegion('Ice Palace')->setBoss(Boss::get("Kholdstare", $world));
}

diff:
@@ -294,6 +294,8 @@ class RandomizerTest extends TestCase
$this->world->getRegion('Ganons Tower')->getBoss('middle')->getEName(),
$this->world->getRegion('Ganons Tower')->getBoss('top')->getEName()]);
+ $this->assertEquals('Kholdstare', $this->world->getRegion('Ice Palace')->getBoss('')->getEName());
+
$this->assertEquals([2, 2, 2, 1, 1, 1, 1, 1, 1, 1],
[
$bosses['Armos'],
@@ -331,6 +333,8 @@ class RandomizerTest extends TestCase
$this->world->getRegion('Ganons Tower')->getBoss('middle')->getEName(),
$this->world->getRegion('Ganons Tower')->getBoss('top')->getEName()]);
+ $this->assertEquals('Kholdstare', $this->world->getRegion('Ice Palace')->getBoss('')->getEName());
+
$this->assertGreaterThanOrEqual(1, $bosses['Armos']);
$this->assertGreaterThanOrEqual(1, $bosses['Lanmola']);
$this->assertGreaterThanOrEqual(1, $bosses['Moldorm']);
@@ -338,7 +342,7 @@ class RandomizerTest extends TestCase
$this->assertGreaterThanOrEqual(1, $bosses['Arrghus']);
$this->assertGreaterThanOrEqual(1, $bosses['Mothula']);
$this->assertGreaterThanOrEqual(1, $bosses['Blind']);
- $this->assertGreaterThanOrEqual(1, $bosses['Kholdstare']);
+ $this->assertEquals(1, $bosses['Kholdstare']);
$this->assertGreaterThanOrEqual(1, $bosses['Vitreous']);
$this->assertGreaterThanOrEqual(1, $bosses['Trinexx']);
}


message: Increase the maximum triforce hunt from 168 to 216.

diff:
@@ -680,9 +680,8 @@ abstract class World
{
$items = [];
- $max_items = 216 - array_sum($this->config('item.advancement'));
foreach ($this->config('item.advancement') as $item_name => $count) {
- $loop = min($this->config('item.count.' . $item_name, $count), $max_items);
+ $loop = min($this->config('item.count.' . $item_name, $count), 216);
for ($i = 0; $i < $loop; ++$i) {
$items[] = $item_name == 'BottleWithRandom' ? $this->getBottle() : Item::get($item_name, $this);
}


message: Copy boss placement for playthrough calculation

diff:
@@ -275,6 +275,24 @@ abstract class World
foreach ($this->shops as $name => $shop) {
$copy->shops[$name] = $shop->copy();
}
+ $boss_locations = [
+ ['Eastern Palace', ''],
+ ['Desert Palace', ''],
+ ['Tower of Hera', ''],
+ ['Palace of Darkness', ''],
+ ['Swamp Palace', ''],
+ ['Skull Woods', ''],
+ ['Thieves Town', ''],
+ ['Ice Palace', ''],
+ ['Misery Mire', ''],
+ ['Turtle Rock', ''],
+ ['Ganons Tower', 'bottom'],
+ ['Ganons Tower', 'middle'],
+ ['Ganons Tower', 'top'],
+ ];
+ foreach ($boss_locations as $location) {
+ $copy->getRegion($location[0])->setBoss($this->getRegion($location[0])->getBoss($location[1]), $location[1]);
+ }
$copy->setPreCollectedItems($this->pre_collected_items->copy());


message: Set escape resources based on starting equipment if Uncle doesn't have a weapon.

diff:
@@ -22,64 +22,7 @@ class Uncle extends Location
public function writeItem(Rom $rom, Item $item = null)
{
parent::writeItem($rom, $item);
- $item = $this->getItem();
-
- $world = $this->region->getWorld();
-
- if ($world->config('mode.state') == 'standard') {
- if ($item instanceof Item\Bow) {
- $rom->setEscapeFills(0b00000001);
- $rom->setUncleSpawnRefills(0, 0, 70);
- $rom->setZeldaSpawnRefills(0, 0, 10);
- $rom->setMantleSpawnRefills(0, 0, 10);
- } elseif ($item == Item::get('TenBombs', $world)) {
- $rom->setEscapeFills(0b00000010);
- $rom->setUncleSpawnRefills(0, 50, 0);
- $rom->setZeldaSpawnRefills(0, 3, 0);
- $rom->setMantleSpawnRefills(0, 3, 0);
- } elseif (
- $item == Item::get('FireRod', $world)
- || $item == Item::get('CaneOfSomaria', $world)
- || $item == Item::get('CaneOfByrna', $world)
- ) {
- $rom->setEscapeFills(0b00000100);
- $rom->setUncleSpawnRefills(0x80, 0, 0);
- $rom->setZeldaSpawnRefills(0x20, 0, 0);
- $rom->setMantleSpawnRefills(0x20, 0, 0);
- } else {
- $rom->setEscapeFills(0b00000000);
- $rom->setUncleSpawnRefills(0, 0, 0);
- $rom->setZeldaSpawnRefills(0, 0, 0);
- $rom->setMantleSpawnRefills(0, 0, 0);
- }
-
- if ($world->config('rom.HardMode') == -1) {
- if ($item instanceof Item\Bow) {
- $rom->setEscapeAssist(0b00000001);
- } elseif ($item == Item::get('TenBombs', $world)) {
- $rom->setEscapeAssist(0b00000010);
- } elseif (
- $item == Item::get('FireRod', $world)
- || $item == Item::get('CaneOfSomaria', $world)
- || $item == Item::get('CaneOfByrna', $world)
- ) {
- $rom->setEscapeAssist(0b00000100);
- } else {
- $rom->setEscapeAssist(0b00000000);
- }
- } else {
- $rom->setEscapeAssist(0b00000000);
- }
- } else {
- $rom->setEscapeFills(0b00000000);
- $rom->setUncleSpawnRefills(0, 0, 0);
- $rom->setZeldaSpawnRefills(0, 0, 0);
- $rom->setMantleSpawnRefills(0, 0, 0);
- $rom->setEscapeAssist(0b00000000);
- }
-
$rom->setCredit('house', $this->getItemCreditsText());
-
return $this;
}

diff:
@@ -1005,6 +1005,10 @@ abstract class World
}
}
+ if ($this->config('mode.state') == 'standard') {
+ $this->setEscapeFills($rom);
+ }
+
$rom->setGoalRequiredCount($this->config('item.Goal.Required', 0) ?: 0);
$rom->setGoalIcon($this->config('item.Goal.Icon', 'triforce'));
@@ -1187,6 +1191,61 @@ abstract class World
return $rom;
}
+ /**
+ * Set the ammo given for escape, based on available weapons
+ *
+ * @param \ALttP\Rom $rom Rom to write data to
+ *
+ * @return void
+ */
+ public function setEscapeFills(Rom $rom) {
+ $uncle_items = new ItemCollection;
+ $uncle_items->setChecksForWorld($this->id);
+ $uncle_items = $uncle_items->addItem($this->getLocation("Link's Uncle")->getItem());
+
+ // Add starting items if uncle doesn't have a weapon. Temporarily disable ignoreCanKillEscapeThings for this check
+ $ignoreCanKillEscapeThings = $this->config('ignoreCanKillEscapeThings', false);
+ $this->config['ignoreCanKillEscapeThings'] = false;
+ if (!$uncle_items->canKillEscapeThings($this)) {
+ $uncle_items = $uncle_items->merge($this->getPreCollectedItems());
+ }
+ $this->config['ignoreCanKillEscapeThings'] = $ignoreCanKillEscapeThings;
+
+ if ($uncle_items->hasSword() || $uncle_items->has('Hammer')) {
+ $rom->setEscapeFills(0b00000000);
+ $rom->setUncleSpawnRefills(0, 0, 0);
+ $rom->setZeldaSpawnRefills(0, 0, 0);
+ $rom->setMantleSpawnRefills(0, 0, 0);
+ } elseif ($uncle_items->has('FireRod')
+ || $uncle_items->has('CaneOfSomaria')
+ || $uncle_items->has('CaneOfByrna')) {
+ $rom->setEscapeFills(0b00000100);
+ $rom->setUncleSpawnRefills(0x80, 0, 0);
+ $rom->setZeldaSpawnRefills(0x20, 0, 0);
+ $rom->setMantleSpawnRefills(0x20, 0, 0);
+ if ($this->config('rom.HardMode') == -1) {
+ $rom->setEscapeAssist(0b00000100);
+ }
+ } elseif ($uncle_items->canShootArrows($this)) {
+ $rom->setEscapeFills(0b00000001);
+ $rom->setUncleSpawnRefills(0, 0, 70);
+ $rom->setZeldaSpawnRefills(0, 0, 10);
+ $rom->setMantleSpawnRefills(0, 0, 10);
+ if ($this->config('rom.HardMode') == -1) {
+ $rom->setEscapeAssist(0b00000001);
+ }
+ } elseif ($uncle_items->has('TenBombs') || $this->config('logic') !== 'None') {
+ // TenBombs, or give player bombs if uncle was plando'd to not have a weapon.
+ $rom->setEscapeFills(0b00000010);
+ $rom->setUncleSpawnRefills(0, 50, 0);
+ $rom->setZeldaSpawnRefills(0, 3, 0);
+ $rom->setMantleSpawnRefills(0, 3, 0);
+ if ($this->config('rom.HardMode') == -1) {
+ $rom->setEscapeAssist(0b00000010);
+ }
+ }
+ }
+
/**
* This is a quick hack to get prizes shuffled, will adjust later when we model sprites.
* this now also handles prize pull trees.


message: Ignore cane of byrna for starting equipment if it isn't in logic to use it as a weapon due to enemy health shuffle.

diff:
@@ -1218,7 +1218,7 @@ abstract class World
$rom->setMantleSpawnRefills(0, 0, 0);
} elseif ($uncle_items->has('FireRod')
|| $uncle_items->has('CaneOfSomaria')
- || $uncle_items->has('CaneOfByrna')) {
+ || ($uncle_items->has('CaneOfByrna') && $this->config('enemizer.enemyHealth', 'default') == 'default')) {
$rom->setEscapeFills(0b00000100);
$rom->setUncleSpawnRefills(0x80, 0, 0);
$rom->setZeldaSpawnRefills(0x20, 0, 0);


message: Fix silvers hint

diff:
@@ -972,7 +972,7 @@ class Randomizer implements RandomizerContract
}
}
// Remove Hint in Hard+ Item Pool
- if ($world->config('item.overflow.count.Bow') < 2) {
+ if ($world->config('item.overflow.count.Bow', 2) < 2) {
$world->setText('ganon_phase_3_no_silvers', "Did you find\nthe arrows on\nPlanet Zebes?");
$world->setText('ganon_phase_3_no_silvers_alt', "Did you find\nthe arrows on\nPlanet Zebes?");
// Special No Silvers "Hint" for Crowd Control

original_message: Fix silvers hint


message: Add Bow and Triforce to advancement items, so they can be randomly placed.

diff:
return [
'advancement' => [
'L1Sword' => 0,
+ //'L1SwordAndShield' => 0, // This still downgrades your sword - don't allow it as a random item unless that's fixed
'MasterSword' => 0,
'ProgressiveSword' => 4,
'BossHeartContainer' => 10,
@@ -15,6 +16,7 @@ return [
'BottleWithFairy' => 0,
'Bombos' => 1,
'BookOfMudora' => 1,
+ 'Bow' => 0,
'BowAndArrows' => 0,
'CaneOfSomaria' => 1,
'Cape' => 1,
@@ -40,6 +42,7 @@ return [
'SilverArrowUpgrade' => 0,
'ProgressiveGlove' => 2,
'ProgressiveBow' => 2,
+ 'Triforce' => 0,
'TriforcePiece' => 0,
'PowerStar' => 0,
'BugCatchingNet' => 1,


message:
Allow non-default dungeon items to be placed randomly if added to the item pool in the customizer.
Hide the "H1" dungeon items from the customizer - the "H2" version should be used instead.
Hyrule castle items in the starting equipment will apply for both the Sewers and Hyrule Castle dungeon ids..

diff:
@@ -104,6 +104,10 @@ class SettingsController extends Controller
'Map',
'MapLW',
'MapDW',
+ 'BigKeyH1',
+ 'KeyH1',
+ 'CompassH1',
+ 'MapH1',
'multiRNG',
'PowerStar',
'singleRNG',


message: Prepare 1.5.0 release.

diff:
@@ -6,35 +6,37 @@ This project adheres to `Semantic Versioning`_ starting with version 0.11.0.
.. _master-release:
-[Unreleased 1.5.0a1] - `master`_
+
+[Unreleased 1.6.0a1] - `master`_
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. note:: This version is not yet released and is under active development.
+
+[1.5.0] - 2019-11-22
+^^^^^^^^^^^^^^^^^^^^
+
Added
-----
- Add support for multiple sanic workers (configurable with the
- ``ACTION_SERVER_SANIC_WORKERS`` environment variable)
-- Add support for async ``run`` methods in the ``Action`` class
-- Return status code ``404`` in case requested action was not found
+ ``ACTION_SERVER_SANIC_WORKERS`` environment variable).
+- Add support for async ``run`` methods in the ``Action`` class.
+- Return status code ``404`` in case requested action was not found.
- Return status code ``400`` in case an empty request body was sent to the ``/webhook``
- endpoint
+ endpoint.
Changed
-------
-- Replace ``flask`` server framework with ``sanic``
-- Replace ``flask_cors`` with ``sanic-cors``
-- ``CollectingDispatcher.utter_message`` can now do anything that other
- dispatcher methods can do.
-- The ``CollectingDispatcher`` methods ``utter_custom_message``,
- ``utter_elements``, ``utter_button_message``, ``utter_attachment``,
- ``utter_button_template``, ``utter_template``, ``utter_custom_json``
- and ``utter_image_url`` were deprecated in favor of ``utter_message``.
+- Replace ``flask`` server framework with ``sanic``.
+- Replace ``flask_cors`` with ``sanic-cors``.
+- ``CollectingDispatcher.utter_message`` can now do anything that other dispatcher
+ methods can do.
+- The ``CollectingDispatcher`` methods ``utter_custom_message``, ``utter_elements``,
+ ``utter_button_message``, ``utter_attachment``, ``utter_button_template``,
+ ``utter_template``, ``utter_custom_json`` and ``utter_image_url`` were deprecated in
+ favor of ``utter_message``.
- Updated format strings to f-strings where appropriate.
-Fixed
------
-
Removed
-------
- Remove ``requests`` dependency

repo: rasahq/rasa-sdk

original_message: Prepare 1.5.0 release.


message: Undo add metadata files

old_path: .gitignore

new_path: .gitignore

diff:
@@ -110,6 +110,3 @@ examples/moodbot/models/
*~
*.DS_Store
-
-.idea
-venv

change_type: DELETE

diff:
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2019 Rasa Technologies GmbH
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.

diff:
-Metadata-Version: 2.1
-Name: rasa-sdk
-Version: 1.6.0
-Summary: Machine learning based dialogue engine for conversational software.
-Home-page: https://rasa.com
-Author: Rasa Technologies GmbH
-Author-email: hi@rasa.com
-Maintainer: Tom Bocklisch
-Maintainer-email: tom@rasa.com
-License: Apache 2.0
-Download-URL: https://github.com/RasaHQ/rasa-sdk/archive/1.6.0.tar.gz
-Project-URL: Bug Reports, https://github.com/rasahq/rasa-sdk/issues
-Project-URL: Documentation, https://rasa.com/docs
-Project-URL: Source, https://github.com/rasahq/rasa-sdk
-Keywords: nlp machine-learning machine-learning-library bot bots botkit rasa conversational-agents conversational-ai chatbotchatbot-framework bot-framework
-Platform: UNKNOWN
-Classifier: Development Status :: 4 - Beta
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.6
-Description-Content-Type: text/markdown
-Requires-Dist: ConfigArgParse (>=0.14)
-Requires-Dist: coloredlogs (~=10.0)
-Requires-Dist: sanic (~=19.9)
-Requires-Dist: sanic-cors (==0.9.9.post1)
-Requires-Dist: multidict (==4.6.1)
-Provides-Extra: test
-Requires-Dist: pytest (~=4.0) ; extra == 'test'
-Requires-Dist: pytest-cov (~=2.0) ; extra == 'test'
-
-# Rasa Python-SDK
-[![Join the chat on Rasa Community Forum](https://img.shields.io/badge/forum-join%20discussions-brightgreen.svg)](https://forum.rasa.com/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-[![Build Status](https://travis-ci.com/RasaHQ/rasa-sdk.svg?branch=master)](https://travis-ci.com/RasaHQ/rasa-sdk)
-[![PyPI version](https://img.shields.io/pypi/v/rasa-sdk.svg)](https://pypi.python.org/pypi/rasa-sdk)
-[![Documentation Status](https://img.shields.io/badge/docs-stable-brightgreen.svg)](https://rasa.com/docs)
-
-Python SDK for the development of custom actions for Rasa.
-
-## Installation
-
-To install the SDK run
-
-```bash
-pip install rasa-sdk
-```
-
-## Compatibility
-
-`rasa-sdk` package:
-
-| SDK version | compatible Rasa version |
-|----------------|-----------------------------------|
-| `1.0.x` | `>=1.0.x` |
-
-old `rasa_core_sdk` package:
-
-| SDK version | compatible Rasa Core version |
-|----------------|----------------------------------------|
-| `0.12.x` | `>=0.12.x` |
-| `0.11.x` | `0.11.x` |
-| not compatible | `<=0.10.x` |
-
-## Usage
-
-Detailed instructions can be found in the Rasa Documentation about
-[Custom Actions](https://rasa.com/docs/rasa/core/actions).
-
-## Docker
-
-### Usage
-
-In order to start an action server using implemented custom actions,
-you can use the available Docker image `rasa/rasa-sdk:latest`.
-
-Before starting the action server ensure that the folder containing
-your actions is handled as Python module and therefore has to contain
-a file called `__init__.py`
-
-Then start the action server using:
-
-```bash
-docker run -p 5055:5055 --mount type=bind,source=<ABSOLUTE_PATH_TO_YOUR_ACTIONS>,target=/app/actions \
- rasa/rasa-sdk:latest
-```
-
-The action server is then available at `http://localhost:5055/webhook`.
-
-### Custom Dependencies
-
-To add custom dependencies you enhance the given Docker image, e.g.:
-
-```
-FROM rasa/rasa-sdk:latest
-
-# To install system dependencies
-RUN apt-get update -qq && \
- apt-get install -y <NAME_OF_REQUIRED_PACKAGE> && \
- apt-get clean && \
- rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-
-# To install packages from PyPI
-RUN pip install --no-cache-dir <A_REQUIRED_PACKAGE_ON_PYPI>
-```
-
-## Code Style
-
-To ensure a standardized code style we use the formatter [black](https://github.com/ambv/black).
-If your code is not formatted properly, travis will fail to build.
-
-If you want to automatically format your code on every commit, you can use [pre-commit](https://pre-commit.com/).
-Just install it via `pip install pre-commit` and execute `pre-commit install`.
-
-If you want to set it up manually, install black via `pip install black`.
-To reformat files execute
-```
-black .
-```
-
-
-## License
-Licensed under the Apache License, Version 2.0. Copyright 2019 Rasa
-Technologies GmbH. [Copy of the license](LICENSE.txt).
-
-A list of the Licenses of the dependencies of the project can be found at
-the bottom of the
-[Libraries Summary](https://libraries.io/github/RasaHQ/rasa-sdk).
-
-

diff:
-rasa_core_sdk
-rasa_sdk
-tests

original_message: Undo add metadata files


message: Apply initial PR feedback

diff:
@@ -209,11 +209,7 @@ class FormAction(Action):
slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
slot_values = {}
- if utils.is_coroutine_action(self.required_slots):
- required_slots = await self.required_slots(tracker)
- else:
- required_slots = self.required_slots(tracker)
- for slot in required_slots:
+ for slot in self.required_slots(tracker):
# look for other slots
if slot != slot_to_fill:
# list is used to cover the case of list slot type
@@ -360,7 +356,7 @@ class FormAction(Action):
return await self.validate_slots(slot_values, dispatcher, tracker, domain)
# noinspection PyUnusedLocal
- async def request_next_slot(
+ def request_next_slot(
self,
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
@@ -369,11 +365,7 @@ class FormAction(Action):
"""Request the next slot and utter template if needed,
else return None"""
- if utils.is_coroutine_action(self.required_slots):
- required_slots = await self.required_slots(tracker)
- else:
- required_slots = self.required_slots(tracker)
- for slot in required_slots:
+ for slot in self.required_slots(tracker):
if self._should_request_slot(tracker, slot):
logger.debug(f"Request next slot '{slot}'")
dispatcher.utter_message(template=f"utter_ask_{slot}", **tracker.slots)
@@ -389,7 +381,7 @@ class FormAction(Action):
logger.debug(f"Deactivating the form '{self.name()}'")
return [Form(None), SlotSet(REQUESTED_SLOT, None)]
- def submit(
+ async def submit(
self,
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
@@ -426,15 +418,10 @@ class FormAction(Action):
return self._to_list(intent), self._to_list(not_intent)
- async def _log_form_slots(self, tracker: "Tracker") -> None:
+ def _log_form_slots(self, tracker: "Tracker") -> None:
"""Logs the values of all required slots before submitting the form."""
-
- if utils.is_coroutine_action(self.required_slots):
- required_slots = await self.required_slots(tracker)
- else:
- required_slots = self.required_slots(tracker)
slot_values = "\n".join(
- [f"\t{slot}: {tracker.get_slot(slot)}" for slot in required_slots]
+ [f"\t{slot}: {tracker.get_slot(slot)}" for slot in self.required_slots(tracker)]
)
logger.debug(
f"No slots left to request, all required slots are filled:\n{slot_values}"

original_message: Apply initial PR feedback


message: Make extract_other_slots sync

diff:
@@ -196,7 +196,7 @@ class FormAction(Action):
return value
# noinspection PyUnusedLocal
- async def extract_other_slots(
+ def extract_other_slots(
self,
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
@@ -337,7 +337,7 @@ class FormAction(Action):
# extract other slots that were not requested
# but set by corresponding entity or trigger intent mapping
- slot_values = await self.extract_other_slots(dispatcher, tracker, domain)
+ slot_values = self.extract_other_slots(dispatcher, tracker, domain)
# extract requested slot
slot_to_fill = tracker.get_slot(REQUESTED_SLOT)

diff:
@@ -390,7 +390,7 @@ def test_extract_requested_slot_from_text_with_not_intent():
assert slot_values == {"some_slot": "some_text"}
-async def test_extract_trigger_slots():
+def test_extract_trigger_slots():
"""Test extraction of a slot value from trigger intent
"""
@@ -423,7 +423,7 @@ async def test_extract_trigger_slots():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was extracted for correct intent
assert slot_values == {"some_slot": "some_value"}
@@ -438,7 +438,7 @@ async def test_extract_trigger_slots():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was not extracted for incorrect intent
assert slot_values == {}
@@ -454,12 +454,12 @@ async def test_extract_trigger_slots():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was not extracted for correct intent
assert slot_values == {}
-async def test_extract_other_slots_no_intent():
+def test_extract_other_slots_no_intent():
"""Test extraction of other not requested slots values
from entities with the same names
"""
@@ -486,7 +486,7 @@ async def test_extract_other_slots_no_intent():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was not extracted for requested slot
assert slot_values == {}
@@ -501,7 +501,7 @@ async def test_extract_other_slots_no_intent():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was extracted for non requested slot
assert slot_values == {"some_other_slot": "some_other_value"}
@@ -521,13 +521,13 @@ async def test_extract_other_slots_no_intent():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was extracted only for non requested slot
assert slot_values == {"some_other_slot": "some_other_value"}
-async def test_extract_other_slots_with_intent():
+def test_extract_other_slots_with_intent():
"""Test extraction of other not requested slots values
from entities with the same names
"""
@@ -564,7 +564,7 @@ async def test_extract_other_slots_with_intent():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was extracted for non requested slot
assert slot_values == {}
@@ -582,7 +582,7 @@ async def test_extract_other_slots_with_intent():
"action_listen",
)
- slot_values = await form.extract_other_slots(CollectingDispatcher(), tracker, {})
+ slot_values = form.extract_other_slots(CollectingDispatcher(), tracker, {})
# check that the value was extracted only for non requested slot
assert slot_values == {"some_other_slot": "some_other_value"}

original_message: Make extract_other_slots sync


message: Add more async/sync tests

diff:
import pytest
import asyncio
+from typing import Type
from rasa_sdk import Tracker, ActionExecutionRejection
from rasa_sdk.events import SlotSet, Form
@@ -767,7 +768,9 @@ async def test_validate_prefilled_slots():
"action_listen",
)
- events = await form._activate_if_required(dispatcher=None, tracker=tracker, domain=None)
+ events = await form._activate_if_required(
+ dispatcher=None, tracker=tracker, domain=None
+ )
# check that the form was activated and prefilled slots were validated
assert events == [
Form("some_form"),
@@ -907,7 +910,9 @@ async def test_activate_if_required():
"action_listen",
)
- events = await form._activate_if_required(dispatcher=None, tracker=tracker, domain=None)
+ events = await form._activate_if_required(
+ dispatcher=None, tracker=tracker, domain=None
+ )
# check that the form was activated
assert events == [Form("some_form")]
@@ -922,7 +927,9 @@ async def test_activate_if_required():
"action_listen",
)
- events = await form._activate_if_required(dispatcher=None, tracker=tracker, domain=None)
+ events = await form._activate_if_required(
+ dispatcher=None, tracker=tracker, domain=None
+ )
# check that the form was not activated again
assert events == []
@@ -1043,9 +1050,7 @@ async def test_deprecated_helper_style():
assert events == [SlotSet("some_slot", "validated_value")]
-async def test_early_deactivation():
- # noinspection PyAbstractClass
- class CustomFormAction(FormAction):
+class FormSyncValidate(FormAction):
def name(self):
return "some_form"
@@ -1056,7 +1061,20 @@ async def test_early_deactivation():
def validate(self, dispatcher, tracker, domain):
return self.deactivate()
- form = CustomFormAction()
+
+class FormAsyncValidate(FormSyncValidate):
+ async def validate(self, dispatcher, tracker, domain):
+ # Not really necessary, just to emphasize this is async
+ await asyncio.sleep(0)
+
+ return self.deactivate()
+
+
+@pytest.mark.parametrize("form_class", [FormSyncValidate, FormAsyncValidate])
+async def test_early_deactivation(form_class: Type[FormAction]):
+ # noinspection PyAbstractClass
+
+ form = form_class()
tracker = Tracker(
"default",
@@ -1074,3 +1092,40 @@ async def test_early_deactivation():
# check that form was deactivated before requesting next slot
assert events == [Form(None), SlotSet("requested_slot", None)]
assert SlotSet("requested_slot", "some_other_slot") not in events
+
+
+class FormSyncSubmit(FormAction):
+ def name(self):
+ return "some_form"
+
+ @staticmethod
+ def required_slots(_tracker):
+ return ["some_slot"]
+
+ def submit(self, dispatcher, tracker, domain):
+ return []
+
+
+class FormAsyncSubmit(FormSyncSubmit):
+ async def submit(self, dispatcher, tracker, domain):
+ # Not really necessary, just to emphasize this is async
+ await asyncio.sleep(0)
+
+ return []
+
+
+@pytest.mark.parametrize("form_class", [FormSyncSubmit, FormAsyncSubmit])
+async def test_submit(form_class: Type[FormAction]):
+ tracker = Tracker(
+ "default",
+ {"some_slot": "foobar"},
+ {"intent": "greet"},
+ [],
+ False,
+ None,
+ {"name": "some_form", "validate": False, "rejected": False},
+ "action_listen",
+ )
+
+ form = form_class()
+ await form.run(dispatcher=None, tracker=tracker, domain=None)

original_message: Add more async/sync tests