Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package values
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// StandardSimplifier returns a simplifier with common property controls.
|
||||
func StandardSimplifier(readOnly bool, fullKey string, unifier Unifier,
|
||||
updater func(func(uint32) uint32), objTypeRenderer ObjectTypeControlRenderer) *interpreters.Simplifier {
|
||||
keys := strings.Split(fullKey, ".")
|
||||
key := keys[len(keys)-1]
|
||||
label := key + "###" + fullKey
|
||||
|
||||
simplifier := interpreters.NewSimplifier(func(minValue, maxValue int64, formatter interpreters.RawValueFormatter) {
|
||||
RenderUnifiedSliderInt(readOnly, label, unifier,
|
||||
func(u Unifier) int {
|
||||
unifiedValue := u.Unified().(int32)
|
||||
if (minValue == -1) && (maxValue == 0x7FFF) {
|
||||
unifiedValue = int32(int16(unifiedValue))
|
||||
}
|
||||
return int(unifiedValue)
|
||||
},
|
||||
func(value int) string {
|
||||
result := formatter(value)
|
||||
if len(result) == 0 {
|
||||
result = "%d"
|
||||
} else {
|
||||
result += " - raw: %d"
|
||||
}
|
||||
return result
|
||||
},
|
||||
int(minValue), int(maxValue),
|
||||
func(newValue int) {
|
||||
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetEnumValueHandler(func(enumValues map[uint32]string) {
|
||||
valueKeys := make([]uint32, 0, len(enumValues))
|
||||
for valueKey := range enumValues {
|
||||
valueKeys = append(valueKeys, valueKey)
|
||||
}
|
||||
sort.Slice(valueKeys, func(indexA, indexB int) bool { return valueKeys[indexA] < valueKeys[indexB] })
|
||||
|
||||
RenderUnifiedCombo(readOnly, label, unifier,
|
||||
func(u Unifier) int {
|
||||
unifiedValue := uint32(u.Unified().(int32))
|
||||
for index, valueKey := range valueKeys {
|
||||
if valueKey == unifiedValue {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
},
|
||||
func(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
}
|
||||
return enumValues[valueKeys[index]]
|
||||
},
|
||||
len(valueKeys),
|
||||
func(newIndex int) {
|
||||
updater(func(oldValue uint32) uint32 { return valueKeys[newIndex] })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetBitfieldHandler(func(maskNames map[uint32]string) {
|
||||
masks := make([]uint32, 0, len(maskNames))
|
||||
for mask := range maskNames {
|
||||
masks = append(masks, mask)
|
||||
}
|
||||
sort.Slice(masks, func(indexA, indexB int) bool { return masks[indexA] < masks[indexB] })
|
||||
|
||||
addMaskedItem := func(mask uint32) {
|
||||
maxValue := mask
|
||||
shift := 0
|
||||
maskedLabel := key + "." + maskNames[mask] + "###" + label + "-" + maskNames[mask]
|
||||
|
||||
for (maxValue & 1) == 0 {
|
||||
shift++
|
||||
maxValue >>= 1
|
||||
}
|
||||
|
||||
if maxValue == 1 {
|
||||
booleanUnifier := NewUnifier()
|
||||
if unifier.IsUnique() {
|
||||
booleanUnifier.Add((uint32(unifier.Unified().(int32)) & mask) != 0)
|
||||
}
|
||||
RenderUnifiedCheckboxCombo(readOnly, maskedLabel, booleanUnifier,
|
||||
func(newValue bool) {
|
||||
updater(func(oldValue uint32) uint32 {
|
||||
result := oldValue & ^mask
|
||||
if newValue {
|
||||
result |= mask
|
||||
}
|
||||
return result
|
||||
})
|
||||
})
|
||||
} else {
|
||||
RenderUnifiedSliderInt(readOnly, maskedLabel, unifier,
|
||||
func(u Unifier) int { return int((uint32(u.Unified().(int32)) & mask) >> uint32(shift)) },
|
||||
func(value int) string { return "%d" },
|
||||
0, int(maxValue),
|
||||
func(newValue int) {
|
||||
updater(func(oldValue uint32) uint32 {
|
||||
return (oldValue & ^mask) | (uint32(newValue) << uint32(shift))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, mask := range masks {
|
||||
addMaskedItem(mask)
|
||||
}
|
||||
})
|
||||
|
||||
simplifier.SetRotationHandler(func(minValue, maxValue int64) {
|
||||
RenderUnifiedRotation(readOnly, label, unifier,
|
||||
int(minValue), int(maxValue),
|
||||
RotationInfo{
|
||||
// This code is specialized for Hacker-Yaw. To re-use it for other rotations, details must change.
|
||||
Horizontal: true,
|
||||
Positive: true,
|
||||
Clockwise: false,
|
||||
},
|
||||
func(newValue int) {
|
||||
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetSpecialHandler("ObjectTriple", func() {
|
||||
var classNames [object.ClassCount]string
|
||||
for index, class := range object.Classes() {
|
||||
classNames[index] = class.String()
|
||||
}
|
||||
tripleResolver := func(u Unifier) object.Triple { return object.TripleFromInt(int(u.Unified().(int32))) }
|
||||
RenderUnifiedCombo(readOnly, key+"-Class###"+fullKey+"-Class", unifier,
|
||||
func(u Unifier) int {
|
||||
triple := tripleResolver(u)
|
||||
return int(triple.Class)
|
||||
},
|
||||
func(value int) string { return fmt.Sprintf("%2d: %v", value, object.Class(value)) },
|
||||
object.ClassCount,
|
||||
func(newValue int) {
|
||||
triple := object.TripleFrom(newValue, 0, 0)
|
||||
updater(func(oldValue uint32) uint32 { return uint32(triple.Int()) })
|
||||
})
|
||||
|
||||
objTypeRenderer.Render(readOnly, key+"###"+fullKey+"-Type", unifier, unifier,
|
||||
tripleResolver,
|
||||
func(newValue object.Triple) {
|
||||
updater(func(oldValue uint32) uint32 { return uint32(newValue.Int()) })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetSpecialHandler("Mistake", func() {})
|
||||
simplifier.SetSpecialHandler("Ignored", func() {})
|
||||
simplifier.SetSpecialHandler("Unknown", func() {})
|
||||
simplifier.SetSpecialHandler("Internal", func() {})
|
||||
|
||||
return simplifier
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package values
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
// ObjectTypeControlRenderer renders a control to select an object.
|
||||
type ObjectTypeControlRenderer struct {
|
||||
// Meta describes the layout of classes, subclasses, and types.
|
||||
Meta object.PropertiesTable
|
||||
// TextCache is used to retrieve the name of a type.
|
||||
TextCache *text.Cache
|
||||
}
|
||||
|
||||
// Render creates the controls according to the given parameters.
|
||||
func (renderer ObjectTypeControlRenderer) Render(readOnly bool, label string,
|
||||
classUnifier Unifier, typeUnifier Unifier,
|
||||
tripleResolver func(Unifier) object.Triple,
|
||||
changeHandler func(object.Triple)) {
|
||||
switch {
|
||||
case classUnifier.IsUnique():
|
||||
class := tripleResolver(classUnifier).Class
|
||||
triples := renderer.Meta.TriplesInClass(class)
|
||||
selectedIndex := -1
|
||||
if typeUnifier.IsUnique() {
|
||||
triple := tripleResolver(typeUnifier)
|
||||
for index, availableTriple := range triples {
|
||||
if availableTriple == triple {
|
||||
selectedIndex = index
|
||||
}
|
||||
}
|
||||
if selectedIndex < 0 {
|
||||
selectedIndex = len(triples)
|
||||
triples = append(triples, triple)
|
||||
}
|
||||
}
|
||||
RenderUnifiedCombo(readOnly, label, typeUnifier,
|
||||
func(u Unifier) int { return selectedIndex },
|
||||
func(value int) string {
|
||||
triple := triples[value]
|
||||
return renderer.tripleName(triple)
|
||||
},
|
||||
len(triples),
|
||||
func(newValue int) {
|
||||
triple := triples[newValue]
|
||||
changeHandler(triple)
|
||||
})
|
||||
case !classUnifier.IsEmpty():
|
||||
imgui.LabelText(label, "(multiple classes)")
|
||||
default:
|
||||
imgui.LabelText(label, "")
|
||||
}
|
||||
}
|
||||
|
||||
func (renderer ObjectTypeControlRenderer) tripleName(triple object.Triple) string {
|
||||
suffix := "???"
|
||||
linearIndex := renderer.Meta.TripleIndex(triple)
|
||||
if (linearIndex >= 0) && (renderer.TextCache != nil) {
|
||||
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, linearIndex)
|
||||
objName, err := renderer.TextCache.Text(key)
|
||||
if err == nil {
|
||||
suffix = objName
|
||||
}
|
||||
}
|
||||
return triple.String() + ": " + suffix
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package values
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
const (
|
||||
hintMultiple = "(multiple)"
|
||||
hintYes = "Yes"
|
||||
hintNo = "No"
|
||||
)
|
||||
|
||||
// RenderUnifiedSliderInt renders a control for an unified integer value.
|
||||
func RenderUnifiedSliderInt(readOnly bool, label string, unifier Unifier,
|
||||
intConverter func(Unifier) int, formatter func(int) string, min, max int, changeHandler func(int)) {
|
||||
var labelValue string
|
||||
var selectedString string
|
||||
selectedValue := -1
|
||||
if unifier.IsUnique() {
|
||||
selectedValue = intConverter(unifier)
|
||||
selectedString = formatter(selectedValue)
|
||||
labelValue = fmt.Sprintf(selectedString, selectedValue)
|
||||
} else if !unifier.IsEmpty() {
|
||||
selectedString = hintMultiple
|
||||
labelValue = selectedString
|
||||
}
|
||||
if readOnly {
|
||||
imgui.LabelText(label, labelValue)
|
||||
} else if gui.StepSliderIntV(label, &selectedValue, min, max, selectedString) {
|
||||
changeHandler(selectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderUnifiedCombo renders a control for a unified enumeration value.
|
||||
func RenderUnifiedCombo(readOnly bool, label string, unifier Unifier,
|
||||
intConverter func(Unifier) int, formatter func(int) string, count int, changeHandler func(int)) {
|
||||
var selectedString string
|
||||
selectedIndex := -1
|
||||
if unifier.IsUnique() {
|
||||
selectedIndex = intConverter(unifier)
|
||||
selectedString = formatter(selectedIndex)
|
||||
} else if !unifier.IsEmpty() {
|
||||
selectedString = hintMultiple
|
||||
}
|
||||
if readOnly {
|
||||
imgui.LabelText(label, selectedString)
|
||||
} else if imgui.BeginCombo(label, selectedString) {
|
||||
for i := 0; i < count; i++ {
|
||||
entryString := formatter(i)
|
||||
|
||||
if imgui.SelectableV(entryString, i == selectedIndex, 0, imgui.Vec2{}) {
|
||||
changeHandler(i)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
}
|
||||
|
||||
// RenderUnifiedCheckboxCombo renders a control for a unified boolean value.
|
||||
func RenderUnifiedCheckboxCombo(readOnly bool, label string, unifier Unifier, changeHandler func(bool)) {
|
||||
selectedString := ""
|
||||
if unifier.IsUnique() {
|
||||
selectedValue := unifier.Unified().(bool)
|
||||
if selectedValue {
|
||||
selectedString = hintYes
|
||||
} else {
|
||||
selectedString = hintNo
|
||||
}
|
||||
} else if !unifier.IsEmpty() {
|
||||
selectedString = hintMultiple
|
||||
}
|
||||
if readOnly {
|
||||
imgui.LabelText(label, selectedString)
|
||||
} else if imgui.BeginCombo(label, selectedString) {
|
||||
for i, text := range []string{hintNo, hintYes} {
|
||||
if imgui.SelectableV(text, text == selectedString, 0, imgui.Vec2{}) {
|
||||
changeHandler(i != 0)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
}
|
||||
|
||||
// RotationInfo describes how the rotation is aligned in its zero position.
|
||||
type RotationInfo struct {
|
||||
Horizontal bool
|
||||
Positive bool
|
||||
Clockwise bool
|
||||
}
|
||||
|
||||
// RenderUnifiedRotation renders a control for rotation value.
|
||||
func RenderUnifiedRotation(readOnly bool, label string, unifier Unifier,
|
||||
min, max int, info RotationInfo, changeHandler func(int)) {
|
||||
calcValuePercent := func(value int) float64 {
|
||||
valueRange := (max - min) + 1
|
||||
return float64(value) / float64(valueRange)
|
||||
}
|
||||
RenderUnifiedSliderInt(readOnly, label, unifier,
|
||||
func(u Unifier) int {
|
||||
unifiedValue := u.Unified().(int32)
|
||||
return int(unifiedValue)
|
||||
},
|
||||
func(value int) string {
|
||||
valuePercent := calcValuePercent(value)
|
||||
result := fmt.Sprintf("%3.02f° - raw: %%d", valuePercent*360.0)
|
||||
return result
|
||||
},
|
||||
min, max,
|
||||
changeHandler)
|
||||
|
||||
if (imgui.IsItemFocused() || imgui.IsItemActive() || imgui.IsItemHovered()) && unifier.IsUnique() {
|
||||
valuePercent := calcValuePercent(int(unifier.Unified().(int32)))
|
||||
valueRadian := valuePercent * (math.Pi * 2)
|
||||
|
||||
lineColor := imgui.Packed(color.RGBA{R: 0x21, G: 0xFF, B: 0x43, A: 0xFF}) // color copied from style.
|
||||
lineThickness := float32(4.0)
|
||||
imgui.BeginTooltip()
|
||||
imgui.Dummy(imgui.Vec2{X: imgui.TextLineHeightWithSpacing() * 2, Y: imgui.TextLineHeightWithSpacing() * 2})
|
||||
dl := imgui.WindowDrawList()
|
||||
winTopLeft := imgui.WindowPos()
|
||||
winSize := imgui.WindowSize()
|
||||
center := winTopLeft.Plus(winSize.Times(0.5))
|
||||
circleRadius := ((winSize.X / 2) * 5) / 6
|
||||
dl.AddCircleV(center, circleRadius, lineColor, 100, lineThickness)
|
||||
|
||||
targetX := 0.0
|
||||
targetY := 0.0
|
||||
if info.Horizontal {
|
||||
targetX = 1.0
|
||||
if !info.Positive {
|
||||
targetX = -1.0
|
||||
}
|
||||
} else {
|
||||
targetY = -1.0
|
||||
if !info.Positive {
|
||||
targetY = 1.0
|
||||
}
|
||||
}
|
||||
var target imgui.Vec2
|
||||
if info.Clockwise {
|
||||
target.X = center.X - float32((float64(circleRadius)*targetX)*math.Cos(valueRadian)+(float64(circleRadius)*targetY)*math.Sin(valueRadian))
|
||||
target.Y = center.Y + float32((float64(circleRadius)*targetY)*math.Cos(valueRadian)-(float64(circleRadius)*targetX)*math.Sin(valueRadian))
|
||||
} else {
|
||||
target.X = center.X + float32((float64(circleRadius)*targetX)*math.Cos(valueRadian)-(float64(circleRadius)*targetY)*math.Sin(valueRadian))
|
||||
target.Y = center.Y - float32((float64(circleRadius)*targetY)*math.Cos(valueRadian)+(float64(circleRadius)*targetX)*math.Sin(valueRadian))
|
||||
}
|
||||
dl.AddLineV(center, target, lineColor, lineThickness)
|
||||
imgui.EndTooltip()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package values
|
||||
|
||||
// Unifier can be used to verify whether a list of values has equal entries.
|
||||
type Unifier struct {
|
||||
state unifierState
|
||||
}
|
||||
|
||||
// NewUnifier returns a new instance.
|
||||
func NewUnifier() Unifier {
|
||||
return Unifier{state: unifierInitState{}}
|
||||
}
|
||||
|
||||
// UnifierFor returns a unifier for a single value.
|
||||
func UnifierFor(value interface{}) Unifier {
|
||||
unifier := NewUnifier()
|
||||
unifier.Add(value)
|
||||
return unifier
|
||||
}
|
||||
|
||||
// Add unifies the given value to the current state.
|
||||
// The given value must be comparable.
|
||||
func (u *Unifier) Add(value interface{}) {
|
||||
u.state = u.state.add(value)
|
||||
}
|
||||
|
||||
// Unified returns the result of the unification.
|
||||
// If all values that were added to the unifier were equal, then the first
|
||||
// added value will be returned. Otherwise, nil will be returned.
|
||||
func (u *Unifier) Unified() interface{} {
|
||||
return u.state.unified()
|
||||
}
|
||||
|
||||
// IsUnique returns true if the unifier has received only equal values.
|
||||
func (u Unifier) IsUnique() bool {
|
||||
return u.state.isUnique()
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the unifier has not received any value.
|
||||
func (u Unifier) IsEmpty() bool {
|
||||
return (u.state == nil) || u.state.isEmpty()
|
||||
}
|
||||
|
||||
type unifierState interface {
|
||||
add(value interface{}) unifierState
|
||||
unified() interface{}
|
||||
isUnique() bool
|
||||
isEmpty() bool
|
||||
}
|
||||
|
||||
type unifierInitState struct{}
|
||||
|
||||
func (state unifierInitState) add(value interface{}) unifierState {
|
||||
return unifierMatchedState{value: value}
|
||||
}
|
||||
|
||||
func (state unifierInitState) unified() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state unifierInitState) isUnique() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (state unifierInitState) isEmpty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type unifierMatchedState struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) add(value interface{}) unifierState {
|
||||
if state.value == value {
|
||||
return state
|
||||
}
|
||||
return unifierMismatchedState{}
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) unified() interface{} {
|
||||
return state.value
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) isUnique() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) isEmpty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type unifierMismatchedState struct{}
|
||||
|
||||
func (state unifierMismatchedState) add(value interface{}) unifierState {
|
||||
return state
|
||||
}
|
||||
|
||||
func (state unifierMismatchedState) unified() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state unifierMismatchedState) isUnique() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (state unifierMismatchedState) isEmpty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package values_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnifier_Integers(t *testing.T) {
|
||||
tt := []struct {
|
||||
values []int
|
||||
expectedValue interface{}
|
||||
expectedUnified bool
|
||||
}{
|
||||
{[]int{1, 1, 1}, 1, true},
|
||||
{[]int{}, nil, false},
|
||||
{[]int{1, 2, 3}, nil, false},
|
||||
}
|
||||
|
||||
for tn, tc := range tt {
|
||||
u := values.NewUnifier()
|
||||
for _, value := range tc.values {
|
||||
u.Add(value)
|
||||
}
|
||||
resultValue := u.Unified()
|
||||
resultUnified := u.IsUnique()
|
||||
assert.Equal(t, tc.expectedValue, resultValue, fmt.Sprintf("Wrong result for test number %d", tn))
|
||||
assert.Equal(t, tc.expectedUnified, resultUnified, fmt.Sprintf("Wrong unified for test number %d", tn))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnifier_Strings(t *testing.T) {
|
||||
tt := []struct {
|
||||
values []string
|
||||
expectedValue interface{}
|
||||
expectedUnified bool
|
||||
}{
|
||||
{[]string{"a", "a", "a"}, "a", true},
|
||||
{[]string{}, nil, false},
|
||||
{[]string{"a", "b", "c"}, nil, false},
|
||||
}
|
||||
|
||||
for tn, tc := range tt {
|
||||
u := values.NewUnifier()
|
||||
for _, value := range tc.values {
|
||||
u.Add(value)
|
||||
}
|
||||
resultValue := u.Unified()
|
||||
resultUnified := u.IsUnique()
|
||||
assert.Equal(t, tc.expectedValue, resultValue, fmt.Sprintf("Wrong result for test number %d", tn))
|
||||
assert.Equal(t, tc.expectedUnified, resultUnified, fmt.Sprintf("Wrong unified for test number %d", tn))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnifier_Types(t *testing.T) {
|
||||
type simpleType struct {
|
||||
value int
|
||||
text string
|
||||
}
|
||||
tt := []struct {
|
||||
values []simpleType
|
||||
expectedValue interface{}
|
||||
expectedUnified bool
|
||||
}{
|
||||
{[]simpleType{{value: 1, text: "a"}, {value: 1, text: "a"}, {value: 1, text: "a"}}, simpleType{value: 1, text: "a"}, true},
|
||||
{[]simpleType{}, nil, false},
|
||||
{[]simpleType{{value: 1, text: "a"}, {value: 1, text: "a"}, {value: 2, text: "a"}}, nil, false},
|
||||
{[]simpleType{{value: 1, text: "a"}, {value: 1, text: "a"}, {value: 1, text: "b"}}, nil, false},
|
||||
}
|
||||
|
||||
for tn, tc := range tt {
|
||||
u := values.NewUnifier()
|
||||
for _, value := range tc.values {
|
||||
u.Add(value)
|
||||
}
|
||||
resultValue := u.Unified()
|
||||
resultUnified := u.IsUnique()
|
||||
assert.Equal(t, tc.expectedValue, resultValue, fmt.Sprintf("Wrong result for test number %d", tn))
|
||||
assert.Equal(t, tc.expectedUnified, resultUnified, fmt.Sprintf("Wrong unified for test number %d", tn))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user