internal/uidriver/glfw: Use glfwGameGamepadState

This change replaces the usage of gamepaddb package with glfwGetGamepadState.

Updates #1557
This commit is contained in:
Hajime Hoshi 2021-07-21 15:08:35 +09:00
parent ee4ec5047e
commit 93a156a718
12 changed files with 155 additions and 483 deletions

View File

@ -74,8 +74,6 @@ const (
StandardGamepadButtonLeftLeft
StandardGamepadButtonLeftRight
StandardGamepadButtonCenterCenter
StandardGamepadButtonMax = StandardGamepadButtonCenterCenter
)
type StandardGamepadAxis int
@ -86,6 +84,4 @@ const (
StandardGamepadAxisLeftStickVertical
StandardGamepadAxisRightStickHorizontal
StandardGamepadAxisRightStickVertical
StandardGamepadAxisMax = StandardGamepadAxisRightStickVertical
)

View File

@ -1,391 +0,0 @@
// Copyright 2021 The Ebiten Authors
//
// 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.
// gamecontrollerdb.txt is downloaded at https://github.com/gabomdq/SDL_GameControllerDB.
//go:generate file2byteslice -package gamepaddb -input=./gamecontrollerdb.txt -output=./gamecontrollerdb.txt.go -var=gamecontrollerdbTxt
package gamepaddb
import (
"bufio"
"bytes"
"fmt"
"io"
"runtime"
"strconv"
"strings"
"github.com/hajimehoshi/ebiten/v2/internal/driver"
)
type platform int
const (
platformUnknown platform = iota
platformWindows
platformMacOS
platformUnix
platformAndroid
platformIOS
)
var additionalGLFWGamepads = []byte(`
78696e70757401000000000000000000,XInput Gamepad (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757402000000000000000000,XInput Wheel (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757403000000000000000000,XInput Arcade Stick (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757404000000000000000000,XInput Flight Stick (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757405000000000000000000,XInput Dance Pad (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757406000000000000000000,XInput Guitar (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757408000000000000000000,XInput Drum Kit (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
`)
func init() {
var currentPlatform platform
if runtime.GOOS == "windows" {
currentPlatform = platformWindows
} else if runtime.GOOS == "aix" ||
runtime.GOOS == "dragonfly" ||
runtime.GOOS == "freebsd" ||
runtime.GOOS == "hurd" ||
runtime.GOOS == "illumos" ||
runtime.GOOS == "linux" ||
runtime.GOOS == "netbsd" ||
runtime.GOOS == "openbsd" ||
runtime.GOOS == "solaris" {
currentPlatform = platformUnix
} else if runtime.GOOS == "android" {
currentPlatform = platformAndroid
} else if isIOS {
currentPlatform = platformIOS
} else if runtime.GOOS == "darwin" {
currentPlatform = platformMacOS
}
if currentPlatform == platformUnknown {
return
}
buf := bytes.NewBuffer(gamecontrollerdbTxt)
buf.Write(additionalGLFWGamepads)
r := bufio.NewReader(buf)
for {
line, err := r.ReadString('\n')
if err != nil && err != io.EOF {
panic(err)
}
if err := processLine(line, currentPlatform); err != nil {
panic(err)
}
if err == io.EOF {
break
}
}
}
type mappingType int
const (
mappingTypeButton mappingType = iota
mappingTypeAxis
mappingTypeHat
)
const (
HatUp = 1
HatRight = 2
HatDown = 4
HatLeft = 8
)
type mapping struct {
Type mappingType
Index int
AxisScale int
AxisOffset int
HatState int
}
var (
gamepadButtonMappings = map[string]map[driver.StandardGamepadButton]*mapping{}
gamepadAxisMappings = map[string]map[driver.StandardGamepadAxis]*mapping{}
)
func processLine(line string, platform platform) error {
line = strings.TrimSpace(line)
if len(line) == 0 {
return nil
}
if line[0] == '#' {
return nil
}
tokens := strings.Split(line, ",")
id := tokens[0]
for _, token := range tokens[2:] {
if len(token) == 0 {
continue
}
tks := strings.Split(token, ":")
if tks[0] == "platform" {
switch tks[1] {
case "Windows":
if platform != platformWindows {
return nil
}
case "Mac OS X":
if platform != platformMacOS {
return nil
}
case "Linux":
if platform != platformUnix {
return nil
}
case "Android":
if platform != platformAndroid {
return nil
}
case "iOS":
if platform != platformIOS {
return nil
}
default:
return fmt.Errorf("gamepaddb: unexpected platform: %s", tks[1])
}
continue
}
gb, err := parseMappingElement(tks[1])
if err != nil {
return err
}
if b, ok := toStandardGamepadButton(tks[0]); ok {
m, ok := gamepadButtonMappings[id]
if !ok {
m = map[driver.StandardGamepadButton]*mapping{}
gamepadButtonMappings[id] = m
}
m[b] = gb
continue
}
if a, ok := toStandardGamepadAxis(tks[0]); ok {
m, ok := gamepadAxisMappings[id]
if !ok {
m = map[driver.StandardGamepadAxis]*mapping{}
gamepadAxisMappings[id] = m
}
m[a] = gb
continue
}
// The buttons like "misc1" are ignored so far.
// There is no corresponding button in the Web standard gamepad layout.
}
return nil
}
func parseMappingElement(str string) (*mapping, error) {
switch {
case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"):
min := -1
max := 1
numstr := str[1:]
if str[0] == '+' {
numstr = str[2:]
min = 0
} else if str[0] == '-' {
numstr = str[2:]
max = 0
}
scale := 2 / (max - min)
offset := -(max + min)
if numstr[len(numstr)-1] == '~' {
numstr = numstr[:len(numstr)-1]
scale = -scale
offset = -offset
}
index, err := strconv.Atoi(numstr)
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeAxis,
Index: index,
AxisScale: scale,
AxisOffset: offset,
}, nil
case str[0] == 'b':
index, err := strconv.Atoi(str[1:])
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeButton,
Index: index,
}, nil
case str[0] == 'h':
tokens := strings.Split(str[1:], ".")
if len(tokens) < 2 {
return nil, fmt.Errorf("gamepaddb: unexpected hat: %s", str)
}
index, err := strconv.Atoi(tokens[0])
if err != nil {
return nil, err
}
hat, err := strconv.Atoi(tokens[1])
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeHat,
Index: index,
HatState: hat,
}, nil
}
return nil, fmt.Errorf("gamepaddb: unepxected mapping: %s", str)
}
func toStandardGamepadButton(str string) (driver.StandardGamepadButton, bool) {
switch str {
case "a":
return driver.StandardGamepadButtonRightBottom, true
case "b":
return driver.StandardGamepadButtonRightRight, true
case "x":
return driver.StandardGamepadButtonRightLeft, true
case "y":
return driver.StandardGamepadButtonRightTop, true
case "back":
return driver.StandardGamepadButtonCenterLeft, true
case "start":
return driver.StandardGamepadButtonCenterRight, true
case "guide":
return driver.StandardGamepadButtonCenterCenter, true
case "leftshoulder":
return driver.StandardGamepadButtonFrontTopLeft, true
case "rightshoulder":
return driver.StandardGamepadButtonFrontTopRight, true
case "leftstick":
return driver.StandardGamepadButtonLeftStick, true
case "rightstick":
return driver.StandardGamepadButtonRightStick, true
case "dpup":
return driver.StandardGamepadButtonLeftTop, true
case "dpright":
return driver.StandardGamepadButtonLeftRight, true
case "dpdown":
return driver.StandardGamepadButtonLeftBottom, true
case "dpleft":
return driver.StandardGamepadButtonLeftLeft, true
case "lefttrigger":
return driver.StandardGamepadButtonFrontBottomLeft, true
case "righttrigger":
return driver.StandardGamepadButtonFrontBottomRight, true
default:
return 0, false
}
}
func toStandardGamepadAxis(str string) (driver.StandardGamepadAxis, bool) {
switch str {
case "leftx":
return driver.StandardGamepadAxisLeftStickHorizontal, true
case "lefty":
return driver.StandardGamepadAxisLeftStickVertical, true
case "rightx":
return driver.StandardGamepadAxisRightStickHorizontal, true
case "righty":
return driver.StandardGamepadAxisRightStickVertical, true
default:
return 0, false
}
}
func HasStandardLayoutMapping(id string) bool {
if _, ok := gamepadButtonMappings[id]; ok {
return true
}
if _, ok := gamepadAxisMappings[id]; ok {
return true
}
return false
}
type GamepadState interface {
Axis(index int) float64
Button(index int) bool
Hat(index int) int
}
func AxisValue(id string, axis driver.StandardGamepadAxis, state GamepadState) float64 {
mappings, ok := gamepadAxisMappings[id]
if !ok {
return 0
}
switch m := mappings[axis]; m.Type {
case mappingTypeAxis:
v := state.Axis(m.Index)*float64(m.AxisScale) + float64(m.AxisOffset)
if v > 1 {
return 1
} else if v < -1 {
return -1
}
return v
case mappingTypeButton:
if state.Button(m.Index) {
return 1
} else {
return -1
}
case mappingTypeHat:
if state.Hat(m.Index)&m.HatState != 0 {
return 1
} else {
return -1
}
}
return 0
}
func IsButtonPressed(id string, button driver.StandardGamepadButton, state GamepadState) bool {
mappings, ok := gamepadButtonMappings[id]
if !ok {
return false
}
switch m := mappings[button]; m.Type {
case mappingTypeAxis:
v := state.Axis(m.Index)*float64(m.AxisScale) + float64(m.AxisOffset)
if m.AxisOffset < 0 || m.AxisOffset == 0 && m.AxisScale > 0 {
return v >= 0
} else {
return v <= 0
}
case mappingTypeButton:
return state.Button(m.Index)
case mappingTypeHat:
return state.Hat(m.Index)&m.HatState != 0
}
return false
}

View File

@ -1,20 +0,0 @@
// Copyright 2021 The Ebiten Authors
//
// 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.
//go:build darwin && ios
// +build darwin,ios
package gamepaddb
const isIOS = true

View File

@ -1,20 +0,0 @@
// Copyright 2021 The Ebiten Authors
//
// 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.
//go:build !darwin || !ios
// +build !darwin !ios
package gamepaddb
const isIOS = false

View File

@ -24,6 +24,8 @@ import (
type (
Action int
ErrorCode int
GamepadAxis int
GamepadButton int
Hint int
InputMode int
Joystick int
@ -167,3 +169,30 @@ const (
HatLeftUp = HatLeft | HatUp
HatLeftDown = HatLeft | HatDown
)
const (
AxisLeftX = GamepadAxis(0)
AxisLeftY = GamepadAxis(1)
AxisRightX = GamepadAxis(2)
AxisRightY = GamepadAxis(3)
AxisLeftTrigger = GamepadAxis(4)
AxisRightTrigger = GamepadAxis(5)
)
const (
ButtonA = GamepadButton(0)
ButtonB = GamepadButton(1)
ButtonX = GamepadButton(2)
ButtonY = GamepadButton(3)
ButtonLeftBumper = GamepadButton(4)
ButtonRightBumper = GamepadButton(5)
ButtonBack = GamepadButton(6)
ButtonStart = GamepadButton(7)
ButtonGuide = GamepadButton(8)
ButtonLeftThumb = GamepadButton(9)
ButtonRightThumb = GamepadButton(10)
ButtonDpadUp = GamepadButton(11)
ButtonDpadRight = GamepadButton(12)
ButtonDpadDown = GamepadButton(13)
ButtonDpadLeft = GamepadButton(14)
)

View File

@ -289,6 +289,19 @@ func (j Joystick) GetHats() []JoystickHatState {
return hats
}
func (j Joystick) GetGamepadState() *GamepadState {
s := glfw.Joystick(j).GetGamepadState()
if s == nil {
return nil
}
state := &GamepadState{}
for i, b := range s.Buttons {
state.Buttons[i] = Action(b)
}
copy(state.Axes[:], s.Axes[:])
return state
}
func GetMonitors() []*Monitor {
ms := []*Monitor{}
for _, m := range glfw.GetMonitors() {
@ -343,6 +356,10 @@ func Terminate() {
glfw.Terminate()
}
func UpdateGamepadMappings(mapping string) bool {
return glfw.UpdateGamepadMappings(mapping)
}
func WindowHint(target Hint, hint int) {
glfw.WindowHint(glfw.Hint(target), hint)
}

View File

@ -405,6 +405,25 @@ func (j Joystick) GetHats() []JoystickHatState {
return hats
}
func (j Joystick) GetGamepadState() *GamepadState {
var s struct {
Buttons [15]uint8
Axes [6]float32
}
r := glfwDLL.call("glfwGetGamepadState", uintptr(j), uintptr(unsafe.Pointer(&s)))
panicError()
if r != True {
return nil
}
state := &GamepadState{}
for i, b := range s.Buttons {
state.Buttons[i] = Action(b)
}
copy(state.Axes[:], s.Axes[:])
return state
}
func GetMonitors() []*Monitor {
var l int32
ptr := glfwDLL.call("glfwGetMonitors", uintptr(unsafe.Pointer(&l)))
@ -497,6 +516,14 @@ func Terminate() {
}
}
func UpdateGamepadMappings(mapping string) bool {
m := append([]byte(mapping), 0)
defer runtime.KeepAlive(m)
r := glfwDLL.call("glfwUpdateGamepadMappings", uintptr(unsafe.Pointer(&m[0])))
panicError()
return r == True
}
func WindowHint(target Hint, hint int) {
glfwDLL.call("glfwWindowHint", uintptr(target), uintptr(hint))
panicError()

View File

@ -33,3 +33,8 @@ type VidMode struct {
BlueBits int
RefreshRate int
}
type GamepadState struct {
Buttons [15]Action
Axes [6]float32
}

View File

@ -17,15 +17,17 @@
// +build !android
// +build !ios
//go:generate file2byteslice -package glfw -input=./gamecontrollerdb.txt -output=./gamecontrollerdb.txt.go -var=gamecontrollerdbTxt
package glfw
import (
"fmt"
"math"
"sync"
"unicode"
"github.com/hajimehoshi/ebiten/v2/internal/driver"
"github.com/hajimehoshi/ebiten/v2/internal/gamepaddb"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
@ -37,8 +39,7 @@ type gamepad struct {
axes [16]float64
buttonNum int
buttonPressed [256]bool
hatsNum int
hats [16]int
state *glfw.GamepadState
}
type Input struct {
@ -317,6 +318,12 @@ func (i *Input) update(window *glfw.Window, context driver.UIContext) {
continue
}
i.gamepads[id].state = id.GetGamepadState()
// Note that GLFW's gamepad GUID follows SDL's GUID.
i.gamepads[id].guid = id.GetGUID()
i.gamepads[id].name = id.GetName()
buttons := id.GetButtons()
// A gamepad can be detected even though there are not. Apparently, some special devices are
@ -346,20 +353,6 @@ func (i *Input) update(window *glfw.Window, context driver.UIContext) {
}
i.gamepads[id].axes[a] = float64(axes32[a])
}
hats := id.GetHats()
i.gamepads[id].hatsNum = len(hats)
for h := 0; h < len(i.gamepads[id].hats); h++ {
if len(hats) <= h {
i.gamepads[id].hats[h] = 0
continue
}
i.gamepads[id].hats[h] = int(hats[h])
}
// Note that GLFW's gamepad GUID follows SDL's GUID.
i.gamepads[id].guid = id.GetGUID()
i.gamepads[id].name = id.GetName()
}
}
@ -371,7 +364,7 @@ func (i *Input) IsStandardGamepadLayoutAvailable(id driver.GamepadID) bool {
return false
}
g := i.gamepads[int(id)]
return gamepaddb.HasStandardLayoutMapping(g.guid)
return g.state != nil
}
func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.StandardGamepadAxis) float64 {
@ -382,7 +375,10 @@ func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.Standa
return 0
}
g := i.gamepads[int(id)]
return gamepaddb.AxisValue(g.guid, axis, &gamepadState{&g})
if g.state == nil {
return 0
}
return float64(g.state.Axes[standardAxisToGLFWAxis(axis)])
}
func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button driver.StandardGamepadButton) bool {
@ -393,37 +389,66 @@ func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button drive
return false
}
g := i.gamepads[int(id)]
return gamepaddb.IsButtonPressed(g.guid, button, &gamepadState{&g})
if g.state == nil {
return false
}
switch button {
case driver.StandardGamepadButtonFrontBottomLeft:
return g.state.Axes[glfw.AxisLeftTrigger] > 0
case driver.StandardGamepadButtonFrontBottomRight:
return g.state.Axes[glfw.AxisRightTrigger] > 0
}
return g.state.Buttons[standardButtonToGLFWButton(button)] == glfw.Press
}
func init() {
// Confirm that all the hat state values are the same.
if gamepaddb.HatUp != glfw.HatUp {
panic("glfw: gamepaddb.HatUp must equal to glfw.HatUp but not")
}
if gamepaddb.HatRight != glfw.HatRight {
panic("glfw: gamepaddb.HatRight must equal to glfw.HatRight but not")
}
if gamepaddb.HatDown != glfw.HatDown {
panic("glfw: gamepaddb.HatDown must equal to glfw.HatDown but not")
}
if gamepaddb.HatLeft != glfw.HatLeft {
panic("glfw: gamepaddb.HatLeft must equal to glfw.HatLeft but not")
func standardAxisToGLFWAxis(axis driver.StandardGamepadAxis) glfw.GamepadAxis {
switch axis {
case driver.StandardGamepadAxisLeftStickHorizontal:
return glfw.AxisLeftX
case driver.StandardGamepadAxisLeftStickVertical:
return glfw.AxisLeftY
case driver.StandardGamepadAxisRightStickHorizontal:
return glfw.AxisRightX
case driver.StandardGamepadAxisRightStickVertical:
return glfw.AxisRightY
default:
panic(fmt.Sprintf("glfw: invalid or inconvertible StandardGamepadAxis: %d", axis))
}
}
type gamepadState struct {
g *gamepad
}
func (s *gamepadState) Axis(index int) float64 {
return s.g.axes[index]
}
func (s *gamepadState) Button(index int) bool {
return s.g.buttonPressed[index]
}
func (s *gamepadState) Hat(index int) int {
return s.g.hats[index]
func standardButtonToGLFWButton(button driver.StandardGamepadButton) glfw.GamepadButton {
switch button {
case driver.StandardGamepadButtonRightBottom:
return glfw.ButtonA
case driver.StandardGamepadButtonRightRight:
return glfw.ButtonB
case driver.StandardGamepadButtonRightLeft:
return glfw.ButtonX
case driver.StandardGamepadButtonRightTop:
return glfw.ButtonY
case driver.StandardGamepadButtonFrontTopLeft:
return glfw.ButtonLeftBumper
case driver.StandardGamepadButtonFrontTopRight:
return glfw.ButtonRightBumper
case driver.StandardGamepadButtonCenterLeft:
return glfw.ButtonBack
case driver.StandardGamepadButtonCenterRight:
return glfw.ButtonStart
case driver.StandardGamepadButtonLeftStick:
return glfw.ButtonLeftThumb
case driver.StandardGamepadButtonRightStick:
return glfw.ButtonRightThumb
case driver.StandardGamepadButtonLeftTop:
return glfw.ButtonDpadUp
case driver.StandardGamepadButtonLeftBottom:
return glfw.ButtonDpadDown
case driver.StandardGamepadButtonLeftLeft:
return glfw.ButtonDpadLeft
case driver.StandardGamepadButtonLeftRight:
return glfw.ButtonDpadRight
case driver.StandardGamepadButtonCenterCenter:
return glfw.ButtonGuide
default:
panic(fmt.Sprintf("glfw: invalid or inconvertible StandardGamepadButton: %d", button))
}
}

View File

@ -172,6 +172,10 @@ func initialize() error {
return err
}
if !glfw.UpdateGamepadMappings(string(gamecontrollerdbTxt)) {
return fmt.Errorf("glfw: UpdateGamepadMappings failed")
}
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)