Unverified Commit 9c028959 authored by Simon Esposito's avatar Simon Esposito Committed by GitHub
Browse files

JavaScript runtime improvements and fixes (#567)

Allow JS function registration to happen inside try/catch blocks.
Correctly handle null return in js match handlers (Resolves #566).
Add user table to Lua functions returning an account but keep the flattened user values for backwards compatibility.
Correctly set user object in accounts returned by js runtime functions.
parent 939bb6e0
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -11,10 +11,12 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
- Do not import Steam friends by default on Steam authentication.
- Do not import Facebook friends by default on Facebook authentication.
- Improve match label update batching semantics.
- Account object returned by some JS runtime functions do not flatten the user values into it anymore.

### Fixed
- Fix an issue in the js runtime that would prevent the matchmaker matched callback to function correctly.
- Allow the console API to return large responses based on the configured max message size.
- Allow JS runtime initializer functions to be invoked inside a try/catch block.

## [3.1.1] - 2021-02-15
### Changed
+191 −144
Original line number Diff line number Diff line
@@ -275,35 +275,42 @@ func (im *RuntimeJavascriptInitModule) registerRpc(r *goja.Runtime) func(goja.Fu
	}
}

func (im *RuntimeJavascriptInitModule) extractRpcFn(r *goja.Runtime, fnName string) (string, error) {
	for _, dec := range im.ast.DeclarationList {
		var fl *ast.FunctionLiteral
		if varDec, ok := dec.(*ast.VariableDeclaration); ok {
			fl, ok = varDec.List[0].Initializer.(*ast.FunctionLiteral)
			if !ok || varDec.List[0].Name != INIT_MODULE_FN_NAME {
				continue
func (im *RuntimeJavascriptInitModule) extractRpcFn(r *goja.Runtime, rpcFnName string) (string, error) {
	bs, initFnVarName, err := im.getInitModuleFn()
	if err != nil {
		return "", err
	}
		} else if fd, ok := dec.(*ast.FunctionDeclaration); ok {
			fl = fd.Function
			if fl.Name.Name != INIT_MODULE_FN_NAME {
				continue

	globalFnId, err := im.getRpcFnIdentifier(r, bs, initFnVarName, rpcFnName)
	if err != nil {
		return "", fmt.Errorf("js %s function key could not be extracted: %s", rpcFnName, err.Error())
	}

	return globalFnId, nil
}

		callerName := fl.ParameterList.List[3].Name
		if l, ok := fl.Body.(*ast.BlockStatement); ok {
			for _, exp := range l.List {
func (im *RuntimeJavascriptInitModule) getRpcFnIdentifier(r *goja.Runtime, bs *ast.BlockStatement, initFnVarName, rpcFnName string) (string, error) {
	for _, exp := range bs.List {
		if try, ok := exp.(*ast.TryStatement); ok {
			if tryBs, ok := try.Body.(*ast.BlockStatement); ok {
				if s, err := im.getRpcFnIdentifier(r, tryBs, initFnVarName, rpcFnName); err != nil {
					continue
				} else {
					return s, nil
				}
			}
		}
		if expStat, ok := exp.(*ast.ExpressionStatement); ok {
			if callExp, ok := expStat.Expression.(*ast.CallExpression); ok {
				if callee, ok := callExp.Callee.(*ast.DotExpression); ok {
							if callee.Left.(*ast.Identifier).Name == callerName && callee.Identifier.Name == "registerRpc" {
					if callee.Left.(*ast.Identifier).Name.String() == initFnVarName && callee.Identifier.Name == "registerRpc" {
						if modNameArg, ok := callExp.ArgumentList[0].(*ast.Identifier); ok {
							id := modNameArg.Name.String()
									if r.Get(id).String() != fnName {
							if r.Get(id).String() != rpcFnName {
								continue
							}
						} else if modNameArg, ok := callExp.ArgumentList[0].(*ast.StringLiteral); ok {
									if modNameArg.Value.String() != fnName {
							if modNameArg.Value.String() != rpcFnName {
								continue
							}
						}
@@ -313,9 +320,7 @@ func (im *RuntimeJavascriptInitModule) extractRpcFn(r *goja.Runtime, fnName stri
						} else if modNameArg, ok := callExp.ArgumentList[1].(*ast.StringLiteral); ok {
							return modNameArg.Value.String(), nil
						} else {
									return "", errors.New("literal function definition: js functions cannot be inlined")
								}
							}
							return "", inlinedFunctionError
						}
					}
				}
@@ -323,7 +328,7 @@ func (im *RuntimeJavascriptInitModule) extractRpcFn(r *goja.Runtime, fnName stri
		}
	}

	return "", errors.New("js rpc function key could not be extracted: key not found")
	return "", errors.New("not found")
}

func (im *RuntimeJavascriptInitModule) registerBeforeGetAccount(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
@@ -876,35 +881,67 @@ func (im *RuntimeJavascriptInitModule) registerHook(r *goja.Runtime, execMode Ru
}

func (im *RuntimeJavascriptInitModule) extractHookFn(registerFnName string) (string, error) {
	for _, dec := range im.ast.DeclarationList {
	bs, initFnVarName, err := im.getInitModuleFn()
	if err != nil {
		return "", err
	}

	globalFnId, err := im.getHookFnIdentifier(bs, initFnVarName, registerFnName)
	if err != nil {
		return "", fmt.Errorf("js %s function key could not be extracted: %s", registerFnName, err.Error())
	}

	return globalFnId, nil
}

func (im *RuntimeJavascriptInitModule) getInitModuleFn() (*ast.BlockStatement, string, error) {
	var fl *ast.FunctionLiteral
	for _, dec := range im.ast.DeclarationList {
		if varDec, ok := dec.(*ast.VariableDeclaration); ok {
			fl, ok = varDec.List[0].Initializer.(*ast.FunctionLiteral)
			if !ok || varDec.List[0].Name != INIT_MODULE_FN_NAME {
				continue
			fnl, ok := varDec.List[0].Initializer.(*ast.FunctionLiteral)
			if ok && varDec.List[0].Name.String() == INIT_MODULE_FN_NAME {
				fl = fnl
				break
			}
		} else if fd, ok := dec.(*ast.FunctionDeclaration); ok {
			if fd.Function.Name.Name.String() == INIT_MODULE_FN_NAME {
				fl = fd.Function
			if fl.Name.Name != INIT_MODULE_FN_NAME {
				continue
				break
			}
		}
	}

	if fl == nil {
		return nil, "", errors.New("failed to find InitModule function")
	}

	fBody := fl.Body.(*ast.BlockStatement)
	initFnName := fl.ParameterList.List[3].Name.String() // Initializer is the 4th argument of InitModule

	return fBody, initFnName, nil
}

		callerName := fl.ParameterList.List[3].Name
		if l, ok := fl.Body.(*ast.BlockStatement); ok {
			for _, exp := range l.List {
func (im *RuntimeJavascriptInitModule) getHookFnIdentifier(bs *ast.BlockStatement, initVarName, registerFnName string) (string, error) {
	for _, exp := range bs.List {
		if try, ok := exp.(*ast.TryStatement); ok {
			if tryBs, ok := try.Body.(*ast.BlockStatement); ok {
				if s, err := im.getHookFnIdentifier(tryBs, initVarName, registerFnName); err != nil {
					continue
				} else {
					return s, nil
				}
			}
		}
		if expStat, ok := exp.(*ast.ExpressionStatement); ok {
			if callExp, ok := expStat.Expression.(*ast.CallExpression); ok {
				if callee, ok := callExp.Callee.(*ast.DotExpression); ok {
							if callee.Left.(*ast.Identifier).Name == callerName && callee.Identifier.Name.String() == registerFnName {
					if callee.Left.(*ast.Identifier).Name.String() == initVarName && callee.Identifier.Name.String() == registerFnName {
						if modNameArg, ok := callExp.ArgumentList[0].(*ast.Identifier); ok {
							return modNameArg.Name.String(), nil
						} else if modNameArg, ok := callExp.ArgumentList[0].(*ast.StringLiteral); ok {
							return modNameArg.Value.String(), nil
						} else {
									return "", inlinedFunctionError
								}
							}
							return "", errors.New("not found")
						}
					}
				}
@@ -912,7 +949,7 @@ func (im *RuntimeJavascriptInitModule) extractHookFn(registerFnName string) (str
		}
	}

	return "", fmt.Errorf("js %s function key could not be extracted: key not found", registerFnName)
	return "", errors.New("not found")
}

func (im *RuntimeJavascriptInitModule) registerRtBefore(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
@@ -980,34 +1017,41 @@ func (im *RuntimeJavascriptInitModule) registerRtAfter(r *goja.Runtime) func(goj
}

func (im *RuntimeJavascriptInitModule) extractRtHookFn(r *goja.Runtime, registerFnName, fnName string) (string, error) {
	for _, dec := range im.ast.DeclarationList {
		var fl *ast.FunctionLiteral
		if varDec, ok := dec.(*ast.VariableDeclaration); ok {
			fl, ok = varDec.List[0].Initializer.(*ast.FunctionLiteral)
			if !ok || varDec.List[0].Name != INIT_MODULE_FN_NAME {
				continue
	bs, initFnVarName, err := im.getInitModuleFn()
	if err != nil {
		return "", err
	}
		} else if fd, ok := dec.(*ast.FunctionDeclaration); ok {
			fl = fd.Function
			if fl.Name.Name != INIT_MODULE_FN_NAME {
				continue

	globalFnId, err := im.getRtHookFnIdentifier(r, bs, initFnVarName, registerFnName, fnName)
	if err != nil {
		return "", fmt.Errorf("js realtime %s hook function key could not be extracted: %s", registerFnName, err.Error())
	}

	return globalFnId, nil
}

		callerName := fl.ParameterList.List[3].Name
		if l, ok := fl.Body.(*ast.BlockStatement); ok {
			for _, exp := range l.List {
func (im *RuntimeJavascriptInitModule) getRtHookFnIdentifier(r *goja.Runtime, bs *ast.BlockStatement, initVarName, registerFnName, rtFnName string) (string, error) {
	for _, exp := range bs.List {
		if try, ok := exp.(*ast.TryStatement); ok {
			if tryBs, ok := try.Body.(*ast.BlockStatement); ok {
				if s, err := im.getRtHookFnIdentifier(r, tryBs, initVarName, registerFnName, rtFnName); err != nil {
					continue
				} else {
					return s, nil
				}
			}
		}
		if expStat, ok := exp.(*ast.ExpressionStatement); ok {
			if callExp, ok := expStat.Expression.(*ast.CallExpression); ok {
				if callee, ok := callExp.Callee.(*ast.DotExpression); ok {
							if callee.Left.(*ast.Identifier).Name == callerName && callee.Identifier.Name.String() == registerFnName {
					if callee.Left.(*ast.Identifier).Name.String() == initVarName && callee.Identifier.Name.String() == registerFnName {
						if modNameArg, ok := callExp.ArgumentList[0].(*ast.Identifier); ok {
							id := modNameArg.Name.String()
									if r.Get(id).String() != fnName {
							if r.Get(id).String() != rtFnName {
								continue
							}
						} else if modNameArg, ok := callExp.ArgumentList[0].(*ast.StringLiteral); ok {
									if modNameArg.Value.String() != fnName {
							if modNameArg.Value.String() != rtFnName {
								continue
							}
						}
@@ -1017,9 +1061,7 @@ func (im *RuntimeJavascriptInitModule) extractRtHookFn(r *goja.Runtime, register
						} else if modNameArg, ok := callExp.ArgumentList[1].(*ast.StringLiteral); ok {
							return modNameArg.Value.String(), nil
						} else {
									return "", inlinedFunctionError
								}
							}
							return "", errors.New("not found")
						}
					}
				}
@@ -1027,7 +1069,7 @@ func (im *RuntimeJavascriptInitModule) extractRtHookFn(r *goja.Runtime, register
		}
	}

	return "", fmt.Errorf("js realtime %s hook function key could not be extracted: key not found", registerFnName)
	return "", errors.New("not found")
}

func (im *RuntimeJavascriptInitModule) registerMatchmakerMatched(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
@@ -1223,28 +1265,35 @@ const (
	MatchTerminate   MatchFnId = "matchTerminate"
)

func (im *RuntimeJavascriptInitModule) extractMatchFnKey(r *goja.Runtime, modName string, id MatchFnId) (string, error) {
	for _, dec := range im.ast.DeclarationList {
		var fl *ast.FunctionLiteral
		if varDec, ok := dec.(*ast.VariableDeclaration); ok {
			fl, ok = varDec.List[0].Initializer.(*ast.FunctionLiteral)
			if !ok || varDec.List[0].Name != INIT_MODULE_FN_NAME {
				continue
func (im *RuntimeJavascriptInitModule) extractMatchFnKey(r *goja.Runtime, modName string, matchFnId MatchFnId) (string, error) {
	bs, initFnVarName, err := im.getInitModuleFn()
	if err != nil {
		return "", err
	}
		} else if fd, ok := dec.(*ast.FunctionDeclaration); ok {
			fl = fd.Function
			if fl.Name.Name != INIT_MODULE_FN_NAME {
				continue

	globalFnId, err := im.getMatchHookFnIdentifier(r, bs, initFnVarName, modName, matchFnId)
	if err != nil {
		return "", fmt.Errorf("js match handler %q function for module %q global id could not be extracted: %s", string(matchFnId), modName, err.Error())
	}

	return globalFnId, nil
}

		callerName := fl.ParameterList.List[3].Name
		if l, ok := fl.Body.(*ast.BlockStatement); ok {
			for _, exp := range l.List {
func (im *RuntimeJavascriptInitModule) getMatchHookFnIdentifier(r *goja.Runtime, bs *ast.BlockStatement, initFnVarName, modName string, matchfnId MatchFnId) (string, error) {
	for _, exp := range bs.List {
		if try, ok := exp.(*ast.TryStatement); ok {
			if tryBs, ok := try.Body.(*ast.BlockStatement); ok {
				if s, err := im.getMatchHookFnIdentifier(r, tryBs, initFnVarName, modName, matchfnId); err != nil {
					continue
				} else {
					return s, nil
				}
			}
		}
		if expStat, ok := exp.(*ast.ExpressionStatement); ok {
			if callExp, ok := expStat.Expression.(*ast.CallExpression); ok {
				if callee, ok := callExp.Callee.(*ast.DotExpression); ok {
							if callee.Left.(*ast.Identifier).Name == callerName && callee.Identifier.Name == "registerMatch" {
					if callee.Left.(*ast.Identifier).Name.String() == initFnVarName && callee.Identifier.Name == "registerMatch" {
						if modNameArg, ok := callExp.ArgumentList[0].(*ast.Identifier); ok {
							id := modNameArg.Name.String()
							if r.Get(id).String() != modName {
@@ -1259,7 +1308,7 @@ func (im *RuntimeJavascriptInitModule) extractMatchFnKey(r *goja.Runtime, modNam
						if obj, ok := callExp.ArgumentList[1].(*ast.ObjectLiteral); ok {
							for _, prop := range obj.Value {
								key, _ := prop.Key.(*ast.StringLiteral)
										if key.Literal == string(id) {
								if key.Literal == string(matchfnId) {
									if sl, ok := prop.Value.(*ast.StringLiteral); ok {
										return sl.Literal, nil
									} else if id, ok := prop.Value.(*ast.Identifier); ok {
@@ -1276,10 +1325,8 @@ func (im *RuntimeJavascriptInitModule) extractMatchFnKey(r *goja.Runtime, modNam
			}
		}
	}
		}
	}

	return "", fmt.Errorf("js match handler %s function key could not be extracted: key not found", id)
	return "", errors.New("not found")
}

func (im *RuntimeJavascriptInitModule) registerCallbackFn(mode RuntimeExecutionMode, key string, fn string) {
+20 −0
Original line number Diff line number Diff line
@@ -262,6 +262,10 @@ func (rm *RuntimeJavaScriptMatchCore) MatchJoinAttempt(tick int64, state interfa
		return nil, false, "", err
	}

	if goja.IsNull(retVal) || goja.IsUndefined(retVal) {
		return nil, false, "", nil
	}

	retMap, ok := retVal.Export().(map[string]interface{})
	if !ok {
		return nil, false, "", errors.New("matchJoinAttempt is expected to return an object with 'state' and 'accept' properties")
@@ -313,6 +317,10 @@ func (rm *RuntimeJavaScriptMatchCore) MatchJoin(tick int64, state interface{}, j
		return nil, err
	}

	if goja.IsNull(retVal) || goja.IsUndefined(retVal) {
		return nil, nil
	}

	retMap, ok := retVal.Export().(map[string]interface{})
	if !ok {
		return nil, errors.New("matchJoin is expected to return an object with 'state' property")
@@ -344,6 +352,10 @@ func (rm *RuntimeJavaScriptMatchCore) MatchLeave(tick int64, state interface{},
		return nil, err
	}

	if goja.IsNull(retVal) || goja.IsUndefined(retVal) {
		return nil, nil
	}

	retMap, ok := retVal.Export().(map[string]interface{})
	if !ok {
		return nil, errors.New("matchLeave is expected to return an object with 'state' property")
@@ -393,6 +405,10 @@ func (rm *RuntimeJavaScriptMatchCore) MatchLoop(tick int64, state interface{}, i
		return nil, nil
	}

	if goja.IsNull(retVal) || goja.IsUndefined(retVal) {
		return nil, nil
	}

	retMap, ok := retVal.Export().(map[string]interface{})
	if !ok {
		return nil, errors.New("matchLoop is expected to return an object with 'state' property")
@@ -418,6 +434,10 @@ func (rm *RuntimeJavaScriptMatchCore) MatchTerminate(tick int64, state interface
		return nil, errors.New("matchTerminate is expected to return an object with 'state' property")
	}

	if goja.IsNull(retVal) || goja.IsUndefined(retVal) {
		return nil, nil
	}

	newState, ok := retMap["state"]
	if !ok {
		return nil, errors.New("matchTerminate is expected to return an object with 'state' property")
+5 −29
Original line number Diff line number Diff line
@@ -5673,38 +5673,14 @@ func getJsBool(r *goja.Runtime, v goja.Value) bool {

func getJsAccountData(account *api.Account) (map[string]interface{}, error) {
	accountData := make(map[string]interface{})
	accountData["userId"] = account.User.Id
	accountData["username"] = account.User.Username
	accountData["displayName"] = account.User.DisplayName
	accountData["avatarUrl"] = account.User.AvatarUrl
	accountData["langTag"] = account.User.LangTag
	accountData["location"] = account.User.Location
	accountData["timezone"] = account.User.Timezone
	if account.User.AppleId != "" {
		accountData["appleId"] = account.User.AppleId
	}
	if account.User.FacebookId != "" {
		accountData["facebookId"] = account.User.FacebookId
	}
	if account.User.FacebookInstantGameId != "" {
		accountData["facebookInstantGameId"] = account.User.FacebookInstantGameId
	}
	if account.User.GoogleId != "" {
		accountData["googleId"] = account.User.GoogleId
	}
	if account.User.GamecenterId != "" {
		accountData["gamecenterId"] = account.User.GamecenterId
	}
	if account.User.SteamId != "" {
		accountData["steamId"] = account.User.SteamId
	userData, err := getJsUserData(account.User)
	if err != nil {
		return nil, err
	}
	accountData["online"] = account.User.Online
	accountData["edgeCount"] = account.User.EdgeCount
	accountData["createTime"] = account.User.CreateTime
	accountData["updateTime"] = account.User.UpdateTime
	accountData["user"] = userData

	metadata := make(map[string]interface{})
	err := json.Unmarshal([]byte(account.User.Metadata), &metadata)
	err = json.Unmarshal([]byte(account.User.Metadata), &metadata)
	if err != nil {
		return nil, fmt.Errorf("failed to convert metadata to json: %s", err.Error())
	}
+70 −53
Original line number Diff line number Diff line
@@ -1775,7 +1775,7 @@ func (n *RuntimeLuaNakamaModule) accountGetId(l *lua.LState) int {
		return 0
	}

	accountTable := l.CreateTable(0, 24)
	accountTable := l.CreateTable(0, 25)
	accountTable.RawSetString("user_id", lua.LString(account.User.Id))
	accountTable.RawSetString("username", lua.LString(account.User.Username))
	accountTable.RawSetString("display_name", lua.LString(account.User.DisplayName))
@@ -1815,6 +1815,13 @@ func (n *RuntimeLuaNakamaModule) accountGetId(l *lua.LState) int {
	metadataTable := RuntimeLuaConvertMap(l, metadataMap)
	accountTable.RawSetString("metadata", metadataTable)

	userTable, err := userToLuaTable(l, account.User)
	if err != nil {
		l.RaiseError(fmt.Sprintf("failed to convert user data to lua table: %s", err.Error()))
		return 0
	}
	accountTable.RawSetString("user", userTable)

	walletMap := make(map[string]int64)
	err = json.Unmarshal([]byte(account.Wallet), &walletMap)
	if err != nil {
@@ -1893,7 +1900,7 @@ func (n *RuntimeLuaNakamaModule) accountsGetId(l *lua.LState) int {

	accountsTable := l.CreateTable(len(accounts), 0)
	for i, account := range accounts {
		accountTable := l.CreateTable(0, 24)
		accountTable := l.CreateTable(0, 25)
		accountTable.RawSetString("user_id", lua.LString(account.User.Id))
		accountTable.RawSetString("username", lua.LString(account.User.Username))
		accountTable.RawSetString("display_name", lua.LString(account.User.DisplayName))
@@ -1933,6 +1940,13 @@ func (n *RuntimeLuaNakamaModule) accountsGetId(l *lua.LState) int {
		metadataTable := RuntimeLuaConvertMap(l, metadataMap)
		accountTable.RawSetString("metadata", metadataTable)

		userTable, err := userToLuaTable(l, account.User)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert user data to lua table: %s", err.Error()))
			return 0
		}
		accountTable.RawSetString("user", userTable)

		walletMap := make(map[string]int64)
		err = json.Unmarshal([]byte(account.Wallet), &walletMap)
		if err != nil {
@@ -2032,62 +2046,61 @@ func (n *RuntimeLuaNakamaModule) usersGetId(l *lua.LState) int {
	}

	// Convert and push the values.
	usersTable, err := usersToLuaTable(l, users.Users)
	usersTable := l.CreateTable(len(users.Users), 0)
	for i, user := range users.Users {
		userTable, err := userToLuaTable(l, user)
		if err != nil {
			l.RaiseError(err.Error())
			return 0
		}
		usersTable.RawSetInt(i+1, userTable)
	}

	l.Push(usersTable)
	return 1
}

func usersToLuaTable(l *lua.LState, users []*api.User) (*lua.LTable, error) {
	usersTable := l.CreateTable(len(users), 0)
	for i, u := range users {
func userToLuaTable(l *lua.LState, user *api.User) (*lua.LTable, error) {
	ut := l.CreateTable(0, 18)
		ut.RawSetString("user_id", lua.LString(u.Id))
		ut.RawSetString("username", lua.LString(u.Username))
		ut.RawSetString("display_name", lua.LString(u.DisplayName))
		ut.RawSetString("avatar_url", lua.LString(u.AvatarUrl))
		ut.RawSetString("lang_tag", lua.LString(u.LangTag))
		ut.RawSetString("location", lua.LString(u.Location))
		ut.RawSetString("timezone", lua.LString(u.Timezone))
		if u.AppleId != "" {
			ut.RawSetString("apple_id", lua.LString(u.AppleId))
	ut.RawSetString("user_id", lua.LString(user.Id))
	ut.RawSetString("username", lua.LString(user.Username))
	ut.RawSetString("display_name", lua.LString(user.DisplayName))
	ut.RawSetString("avatar_url", lua.LString(user.AvatarUrl))
	ut.RawSetString("lang_tag", lua.LString(user.LangTag))
	ut.RawSetString("location", lua.LString(user.Location))
	ut.RawSetString("timezone", lua.LString(user.Timezone))
	if user.AppleId != "" {
		ut.RawSetString("apple_id", lua.LString(user.AppleId))
	}
		if u.FacebookId != "" {
			ut.RawSetString("facebook_id", lua.LString(u.FacebookId))
	if user.FacebookId != "" {
		ut.RawSetString("facebook_id", lua.LString(user.FacebookId))
	}
		if u.FacebookInstantGameId != "" {
			ut.RawSetString("facebook_instant_game_id", lua.LString(u.FacebookInstantGameId))
	if user.FacebookInstantGameId != "" {
		ut.RawSetString("facebook_instant_game_id", lua.LString(user.FacebookInstantGameId))
	}
		if u.GoogleId != "" {
			ut.RawSetString("google_id", lua.LString(u.GoogleId))
	if user.GoogleId != "" {
		ut.RawSetString("google_id", lua.LString(user.GoogleId))
	}
		if u.GamecenterId != "" {
			ut.RawSetString("gamecenter_id", lua.LString(u.GamecenterId))
	if user.GamecenterId != "" {
		ut.RawSetString("gamecenter_id", lua.LString(user.GamecenterId))
	}
		if u.SteamId != "" {
			ut.RawSetString("steam_id", lua.LString(u.SteamId))
	if user.SteamId != "" {
		ut.RawSetString("steam_id", lua.LString(user.SteamId))
	}
		ut.RawSetString("online", lua.LBool(u.Online))
		ut.RawSetString("edge_count", lua.LNumber(u.EdgeCount))
		ut.RawSetString("create_time", lua.LNumber(u.CreateTime.Seconds))
		ut.RawSetString("update_time", lua.LNumber(u.UpdateTime.Seconds))
	ut.RawSetString("online", lua.LBool(user.Online))
	ut.RawSetString("edge_count", lua.LNumber(user.EdgeCount))
	ut.RawSetString("create_time", lua.LNumber(user.CreateTime.Seconds))
	ut.RawSetString("update_time", lua.LNumber(user.UpdateTime.Seconds))

	metadataMap := make(map[string]interface{})
		err := json.Unmarshal([]byte(u.Metadata), &metadataMap)
	err := json.Unmarshal([]byte(user.Metadata), &metadataMap)
	if err != nil {
		return nil, fmt.Errorf("failed to convert metadata to json: %s", err.Error())
	}
	metadataTable := RuntimeLuaConvertMap(l, metadataMap)
	ut.RawSetString("metadata", metadataTable)

		usersTable.RawSetInt(i+1, ut)
	}

	return usersTable, nil
	return ut, nil
}

func (n *RuntimeLuaNakamaModule) usersGetUsername(l *lua.LState) int {
@@ -2130,11 +2143,15 @@ func (n *RuntimeLuaNakamaModule) usersGetUsername(l *lua.LState) int {
	}

	// Convert and push the values.
	usersTable, err := usersToLuaTable(l, users.Users)
	usersTable := l.CreateTable(len(users.Users), 0)
	for i, user := range users.Users {
		userTable, err := userToLuaTable(l, user)
		if err != nil {
			l.RaiseError(err.Error())
			return 0
		}
		usersTable.RawSetInt(i+1, userTable)
	}

	l.Push(usersTable)
	return 1