Basic synced player movement
This commit is contained in:
parent
f85469dad4
commit
07f02d5a32
7 changed files with 193 additions and 112 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,5 +9,6 @@
|
||||||
debug/
|
debug/
|
||||||
release/
|
release/
|
||||||
target/
|
target/
|
||||||
|
*.tmp
|
||||||
|
|
||||||
lib/
|
lib/
|
||||||
116
client/danmaku!/network_manager.gd
Normal file
116
client/danmaku!/network_manager.gd
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
extends Node
|
||||||
|
var nakama_client: NakamaClient
|
||||||
|
var nakama_session: NakamaSession
|
||||||
|
var nakama_socket: NakamaSocket
|
||||||
|
|
||||||
|
const SERVER_WIDTH = 90.0
|
||||||
|
const SERVER_HEIGHT = 160.0
|
||||||
|
|
||||||
|
var predicted_tick = 0
|
||||||
|
var delta_counter = 0
|
||||||
|
var bullet_lerp_factor := 0.0
|
||||||
|
var bullets = []
|
||||||
|
var current_match_id = ""
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
print("Attempting auth.")
|
||||||
|
await attempt_auth()
|
||||||
|
print("Attempting to create debug match.")
|
||||||
|
await create_and_join_debug_match()
|
||||||
|
nakama_socket.received_match_state.connect(self._on_match_state)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if current_match_id == "":
|
||||||
|
return
|
||||||
|
|
||||||
|
predict_tick_and_broadcast(delta)
|
||||||
|
|
||||||
|
for bullet in bullets:
|
||||||
|
var prev_pos = bullet.get_current_pos(predicted_tick)
|
||||||
|
var next_pos = bullet.get_current_pos(predicted_tick + 1)
|
||||||
|
var interpolated_pos = prev_pos.lerp(next_pos, bullet_lerp_factor)
|
||||||
|
|
||||||
|
bullet.position = world_to_screen(interpolated_pos)
|
||||||
|
|
||||||
|
#var screen_size = get_viewport().size
|
||||||
|
#bullets = bullets.filter(func(bullet):
|
||||||
|
# return bullet.position.x >= 0 and bullet.position.x <= screen_size.x and \
|
||||||
|
# bullet.position.y >= 0 and bullet.position.y <= screen_size.y
|
||||||
|
#)
|
||||||
|
|
||||||
|
func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
||||||
|
match p_state.op_code:
|
||||||
|
2: # Server state update
|
||||||
|
var data = JSON.parse_string(p_state.data)
|
||||||
|
var bullet = DanmakuBullet.new()
|
||||||
|
bullet.setup_bullet(
|
||||||
|
int(data["class"]),
|
||||||
|
int(data["tick"]),
|
||||||
|
data["x"],
|
||||||
|
data["y"],
|
||||||
|
data["vel_x"],
|
||||||
|
data["vel_y"])
|
||||||
|
bullet.position = world_to_screen(bullet.get_current_pos(int(data["tick"])))
|
||||||
|
bullet.texture = load("res://test-bullet.png")
|
||||||
|
add_child(bullet)
|
||||||
|
bullets.append(bullet)
|
||||||
|
#delta_counter = 0
|
||||||
|
#predicted_tick = int(data["tick"])
|
||||||
|
|
||||||
|
func attempt_auth() -> void:
|
||||||
|
nakama_client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http")
|
||||||
|
nakama_session = await nakama_client.authenticate_device_async(OS.get_unique_id())
|
||||||
|
nakama_socket = Nakama.create_socket_from(nakama_client)
|
||||||
|
|
||||||
|
var connected: NakamaAsyncResult = await nakama_socket.connect_async(nakama_session)
|
||||||
|
if connected.is_exception():
|
||||||
|
print("An error occured when creating nakama socket: %s" % connected)
|
||||||
|
return
|
||||||
|
print("Oh baby we're ready.")
|
||||||
|
|
||||||
|
func create_and_join_debug_match() -> void:
|
||||||
|
var response: NakamaAPI.ApiRpc = await nakama_client.rpc_async(nakama_session, "manual_force_create_br_match_rpc")
|
||||||
|
|
||||||
|
if response.is_exception():
|
||||||
|
print("An error occurred when calling manual_force_create_br_match_rpc: %s" % response)
|
||||||
|
return
|
||||||
|
|
||||||
|
var debug_br_match: NakamaRTAPI.Match = await nakama_socket.join_match_async(response.payload)
|
||||||
|
|
||||||
|
if debug_br_match.is_exception():
|
||||||
|
print("An error occurred when joining debug BR match: %s" % response)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
current_match_id = response.payload
|
||||||
|
|
||||||
|
func world_to_screen(server_pos: Vector2) -> Vector2:
|
||||||
|
var screen_size = get_viewport().size
|
||||||
|
var scale_x = screen_size.x / SERVER_WIDTH
|
||||||
|
var scale_y = screen_size.y / SERVER_HEIGHT
|
||||||
|
|
||||||
|
var client_x = server_pos.x * scale_x
|
||||||
|
var client_y = server_pos.y * scale_y
|
||||||
|
|
||||||
|
return Vector2(client_x, client_y)
|
||||||
|
|
||||||
|
func screen_to_world(client_pos: Vector2) -> Vector2:
|
||||||
|
var screen_size = get_viewport().size
|
||||||
|
var scale_x = SERVER_WIDTH / screen_size.x
|
||||||
|
var scale_y = SERVER_HEIGHT / screen_size.y
|
||||||
|
|
||||||
|
var server_x = client_pos.x * scale_x
|
||||||
|
var server_y = client_pos.y * scale_y
|
||||||
|
|
||||||
|
return Vector2(server_x, server_y)
|
||||||
|
|
||||||
|
func predict_tick_and_broadcast(delta):
|
||||||
|
delta_counter += delta
|
||||||
|
|
||||||
|
# New tick, broadcast player inputs
|
||||||
|
if delta_counter >= 0.05:
|
||||||
|
predicted_tick += 1
|
||||||
|
delta_counter = 0
|
||||||
|
var position = screen_to_world(get_node("../Player").position)
|
||||||
|
var json_string = JSON.stringify({"x": position.x, "y": position.y})
|
||||||
|
nakama_socket.send_match_state_async(current_match_id, 0, json_string)
|
||||||
|
bullet_lerp_factor = delta_counter / 0.05
|
||||||
|
|
@ -1,95 +1,17 @@
|
||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
var nakama_client: NakamaClient
|
@export var speed = 400
|
||||||
var nakama_session: NakamaSession
|
var velocity = 0
|
||||||
var nakama_socket: NakamaSocket
|
|
||||||
|
|
||||||
const SERVER_WIDTH = 90.0
|
func get_input():
|
||||||
const SERVER_HEIGHT = 160.0
|
if Input.is_action_pressed("Slow Mode"):
|
||||||
|
speed = 200
|
||||||
|
else:
|
||||||
|
speed = 400
|
||||||
|
|
||||||
var predicted_tick = 0
|
var input_direction = Input.get_vector("Left", "Right", "Up", "Down")
|
||||||
var delta_counter = 0
|
velocity = input_direction * speed
|
||||||
var lerp_factor := 0.0
|
|
||||||
var bullets = []
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _physics_process(delta):
|
||||||
print("Attempting auth.")
|
get_input()
|
||||||
await attempt_auth()
|
position += velocity * delta
|
||||||
print("Attempting to create debug match.")
|
|
||||||
await create_and_join_debug_match()
|
|
||||||
nakama_socket.received_match_state.connect(self._on_match_state)
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
delta_counter += delta
|
|
||||||
|
|
||||||
if delta_counter >= 0.05:
|
|
||||||
predicted_tick += 1
|
|
||||||
delta_counter = 0 # Reset counter
|
|
||||||
lerp_factor = delta_counter / 0.05 # Normalize factor between 0 and 1
|
|
||||||
|
|
||||||
for bullet in bullets:
|
|
||||||
var prev_pos = bullet.get_current_pos(predicted_tick)
|
|
||||||
var next_pos = bullet.get_current_pos(predicted_tick + 1)
|
|
||||||
var interpolated_pos = prev_pos.lerp(next_pos, lerp_factor)
|
|
||||||
|
|
||||||
bullet.position = world_to_screen(interpolated_pos)
|
|
||||||
|
|
||||||
#var screen_size = get_viewport().size
|
|
||||||
#bullets = bullets.filter(func(bullet):
|
|
||||||
# return bullet.position.x >= 0 and bullet.position.x <= screen_size.x and \
|
|
||||||
# bullet.position.y >= 0 and bullet.position.y <= screen_size.y
|
|
||||||
#)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
|
||||||
match p_state.op_code:
|
|
||||||
2: # Spawn bullet
|
|
||||||
var data = JSON.parse_string(p_state.data)
|
|
||||||
var bullet = DanmakuBullet.new()
|
|
||||||
bullet.setup_bullet(
|
|
||||||
int(data["class"]),
|
|
||||||
int(data["tick"]),
|
|
||||||
data["x"],
|
|
||||||
data["y"],
|
|
||||||
data["vel_x"],
|
|
||||||
data["vel_y"])
|
|
||||||
bullet.position = world_to_screen(bullet.get_current_pos(int(data["tick"])))
|
|
||||||
bullet.texture = load("res://test-bullet.png")
|
|
||||||
add_child(bullet)
|
|
||||||
bullets.append(bullet)
|
|
||||||
delta_counter = 0
|
|
||||||
predicted_tick = int(data["tick"])
|
|
||||||
|
|
||||||
func attempt_auth() -> void:
|
|
||||||
nakama_client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http")
|
|
||||||
nakama_session = await nakama_client.authenticate_device_async(OS.get_unique_id())
|
|
||||||
nakama_socket = Nakama.create_socket_from(nakama_client)
|
|
||||||
|
|
||||||
var connected: NakamaAsyncResult = await nakama_socket.connect_async(nakama_session)
|
|
||||||
if connected.is_exception():
|
|
||||||
print("An error occured when creating nakama socket: %s" % connected)
|
|
||||||
return
|
|
||||||
print("Oh baby we're ready.")
|
|
||||||
|
|
||||||
func create_and_join_debug_match() -> void:
|
|
||||||
var response: NakamaAPI.ApiRpc = await nakama_client.rpc_async(nakama_session, "manual_force_create_br_match_rpc")
|
|
||||||
|
|
||||||
if response.is_exception():
|
|
||||||
print("An error occurred when calling manual_force_create_br_match_rpc: %s" % response)
|
|
||||||
return
|
|
||||||
|
|
||||||
var debug_br_match: NakamaRTAPI.Match = await nakama_socket.join_match_async(response.payload)
|
|
||||||
|
|
||||||
if debug_br_match.is_exception():
|
|
||||||
print("An error occurred when joining debug BR match: %s" % response)
|
|
||||||
return
|
|
||||||
|
|
||||||
func world_to_screen(server_pos: Vector2) -> Vector2:
|
|
||||||
var screen_size = get_viewport().size
|
|
||||||
var scale_x = screen_size.x / SERVER_WIDTH
|
|
||||||
var scale_y = screen_size.y / SERVER_HEIGHT
|
|
||||||
|
|
||||||
var client_x = server_pos.x * scale_x
|
|
||||||
var client_y = server_pos.y * scale_y
|
|
||||||
|
|
||||||
return Vector2(client_x, client_y)
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
[gd_scene load_steps=2 format=3 uid="uid://cd3tqt7hr5pqs"]
|
[gd_scene load_steps=3 format=3 uid="uid://cd3tqt7hr5pqs"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://danmaku!/player.gd" id="1_l6typ"]
|
[ext_resource type="Script" path="res://danmaku!/player.gd" id="1_l6typ"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bs3fntlmlqpt2" path="res://icon.svg" id="2_j7sx3"]
|
||||||
|
|
||||||
[node name="Player" type="Node2D"]
|
[node name="Player" type="Node2D"]
|
||||||
script = ExtResource("1_l6typ")
|
script = ExtResource("1_l6typ")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
texture = ExtResource("2_j7sx3")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
[gd_scene load_steps=2 format=3 uid="uid://b1m2pclbncn68"]
|
[gd_scene load_steps=3 format=3 uid="uid://b1m2pclbncn68"]
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://cd3tqt7hr5pqs" path="res://danmaku!/player.tscn" id="1_jeq34"]
|
[ext_resource type="PackedScene" uid="uid://cd3tqt7hr5pqs" path="res://danmaku!/player.tscn" id="1_jeq34"]
|
||||||
|
[ext_resource type="Script" path="res://danmaku!/network_manager.gd" id="2_3453q"]
|
||||||
|
|
||||||
[node name="Testworld" type="Node2D"]
|
[node name="Testworld" type="Node2D"]
|
||||||
|
|
||||||
[node name="Player" parent="." instance=ExtResource("1_jeq34")]
|
[node name="Player" parent="." instance=ExtResource("1_jeq34")]
|
||||||
|
|
||||||
|
[node name="NetworkManager" type="Node" parent="."]
|
||||||
|
script = ExtResource("2_3453q")
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,31 @@ Nakama="*res://addons/com.heroiclabs.nakama/Nakama.gd"
|
||||||
animation_library={
|
animation_library={
|
||||||
"animation/fps": 120.0
|
"animation/fps": 120.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[input]
|
||||||
|
|
||||||
|
Left={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Right={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Up={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Down={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"Slow Mode"={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
|
@ -22,7 +21,8 @@ import (
|
||||||
const (
|
const (
|
||||||
MATCH_LOADING = iota
|
MATCH_LOADING = iota
|
||||||
MATCH_START
|
MATCH_START
|
||||||
SPAWN_BULLET
|
STATE_UPDATE
|
||||||
|
FINAL_PHASE
|
||||||
MATCH_END
|
MATCH_END
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,6 +35,11 @@ type PlayerStageState struct {
|
||||||
bullets []*C.Bullet
|
bullets []*C.Bullet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlayerMessageData struct {
|
||||||
|
X float64 `json:"x"`
|
||||||
|
Y float64 `json:"y"`
|
||||||
|
}
|
||||||
|
|
||||||
type PresenceState struct { // present time! hahahahahahahah!
|
type PresenceState struct { // present time! hahahahahahahah!
|
||||||
presence runtime.Presence
|
presence runtime.Presence
|
||||||
stageState PlayerStageState
|
stageState PlayerStageState
|
||||||
|
|
@ -156,7 +161,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
||||||
// Test bullet spawning
|
// Test bullet spawning
|
||||||
if tick%1 == 0 {
|
if tick%1 == 0 {
|
||||||
for _, v := range lobbyState.presences {
|
for _, v := range lobbyState.presences {
|
||||||
vel := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
|
velx := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
|
||||||
|
vely := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
|
||||||
vel_x_sign := 2*rand.Intn(2) - 1
|
vel_x_sign := 2*rand.Intn(2) - 1
|
||||||
vel_y_sign := 2*rand.Intn(2) - 1
|
vel_y_sign := 2*rand.Intn(2) - 1
|
||||||
|
|
||||||
|
|
@ -165,8 +171,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
||||||
C.int64_t(tick),
|
C.int64_t(tick),
|
||||||
C.double(STAGE_WIDTH*rand.Float64()),
|
C.double(STAGE_WIDTH*rand.Float64()),
|
||||||
C.double(STAGE_HEIGHT*rand.Float64()),
|
C.double(STAGE_HEIGHT*rand.Float64()),
|
||||||
C.double(float64(vel_x_sign)*vel),
|
C.double(float64(vel_x_sign)*velx),
|
||||||
C.double(float64(vel_y_sign)*vel),
|
C.double(float64(vel_y_sign)*vely),
|
||||||
)
|
)
|
||||||
|
|
||||||
var x, y C.double
|
var x, y C.double
|
||||||
|
|
@ -177,8 +183,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
||||||
"tick": tick,
|
"tick": tick,
|
||||||
"x": float64(x),
|
"x": float64(x),
|
||||||
"y": float64(y),
|
"y": float64(y),
|
||||||
"vel_x": float64(vel_x_sign) * vel,
|
"vel_x": float64(vel_x_sign) * velx,
|
||||||
"vel_y": float64(vel_y_sign) * vel,
|
"vel_y": float64(vel_y_sign) * vely,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(bulletData)
|
data, err := json.Marshal(bulletData)
|
||||||
|
|
@ -187,19 +193,25 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
||||||
} else {
|
} else {
|
||||||
v.stageState.bullets = append(v.stageState.bullets, bullet)
|
v.stageState.bullets = append(v.stageState.bullets, bullet)
|
||||||
reliable := true
|
reliable := true
|
||||||
dispatcher.BroadcastMessage(SPAWN_BULLET, data, nil, nil, reliable)
|
dispatcher.BroadcastMessage(STATE_UPDATE, data, nil, nil, reliable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bullet cleanup
|
// Respond to player input
|
||||||
for _, v := range lobbyState.presences {
|
for _, msg := range messages {
|
||||||
for _, bullet := range v.stageState.bullets {
|
var pos PlayerMessageData
|
||||||
var x, y C.double
|
if err := json.Unmarshal(msg.GetData(), &pos); err != nil {
|
||||||
C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y)
|
logger.Warn("Failed to parse input: %v", err)
|
||||||
fmt.Printf("Bullet at (%.2f, %.2f)\n", float64(x), float64(y))
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lobbyState.presences[msg.GetSessionId()].stageState.xPos = pos.X
|
||||||
|
lobbyState.presences[msg.GetSessionId()].stageState.yPos = pos.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bullet cleanup when off board
|
||||||
|
for _, v := range lobbyState.presences {
|
||||||
v.stageState.bullets = slices.DeleteFunc(v.stageState.bullets, func(b *C.Bullet) bool {
|
v.stageState.bullets = slices.DeleteFunc(v.stageState.bullets, func(b *C.Bullet) bool {
|
||||||
if C.bullet_beyond_kill_boundary(b, C.int64_t(tick)) {
|
if C.bullet_beyond_kill_boundary(b, C.int64_t(tick)) {
|
||||||
C.destroy_bullet(b)
|
C.destroy_bullet(b)
|
||||||
|
|
@ -214,12 +226,6 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
||||||
|
|
||||||
// RPC for force-creating a match for debugging/development, separate from the matchmaking process
|
// RPC for force-creating a match for debugging/development, separate from the matchmaking process
|
||||||
func ManualForceCreateBRMatchRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
func ManualForceCreateBRMatchRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
||||||
/*params := make(map[string]interface{})
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(payload), ¶ms); err != nil {
|
|
||||||
return "", err
|
|
||||||
}*/
|
|
||||||
|
|
||||||
modulename := "battle-royale"
|
modulename := "battle-royale"
|
||||||
|
|
||||||
if matchId, err := nk.MatchCreate(ctx, modulename, make(map[string]interface{})); err != nil {
|
if matchId, err := nk.MatchCreate(ctx, modulename, make(map[string]interface{})); err != nil {
|
||||||
|
|
|
||||||
Reference in a new issue