/** * Copyright (C) 2009-2014 Cars and Tracks Development Project (CTDP). * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package net.ctdp.rfdynhud.input; import java.io.File; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import net.ctdp.rfdynhud.RFDynHUD; import net.ctdp.rfdynhud.gamedata.GameEventsManager; import net.ctdp.rfdynhud.gamedata.GameFileSystem; import net.ctdp.rfdynhud.gamedata.LiveGameData; import net.ctdp.rfdynhud.gamedata.__GDPrivilegedAccess; import net.ctdp.rfdynhud.render.WidgetsDrawingManager; import net.ctdp.rfdynhud.util.RFDHLog; import org.jagatoo.util.errorhandling.ParsingException; import org.jagatoo.util.ini.AbstractIniParser; /** * This manager manages mappings of {@link InputAction}s to input device components (buttons and keys). * * @author Marvin Froehlich (CTDP) */ public class InputMappingsManager { public static final String CONFIG_FILE_NAME = "input_bindings.ini"; private final RFDynHUD rfDynHUD; private InputMapping[] mappings = null; private ByteBuffer buffer = null; private int numDevices = 0; private short[] bufferOffsets = null; private int[] numComponents = null; private byte[][] states0 = null; private byte[][] states1 = null; private boolean isPluginEnabled = true; public final ByteBuffer getBuffer() { return ( buffer ); } public final boolean isPluginEnabled() { return ( isPluginEnabled ); } /* public void clearKnownActions() { knownActions.clear(); } public void addKnownActions( InputAction[] actions ) { for ( int i = 0; i < actions.length; i++ ) { knownActions.put( actions[i].getName(), actions[i] ); } } */ private static final int parseModifierMask( String[] parts ) { int modifierMask = 0; for ( int i = 0; i < parts.length - 1; i++ ) { if ( parts[i].equals( "SHIFT" ) ) modifierMask |= InputMapping.MODIFIER_MASK_SHIFT; else if ( parts[i].equals( "CTRL" ) ) modifierMask |= InputMapping.MODIFIER_MASK_CTRL; else if ( parts[i].equals( "LALT" ) ) modifierMask |= InputMapping.MODIFIER_MASK_LALT; else if ( parts[i].equals( "RALT" ) ) modifierMask |= InputMapping.MODIFIER_MASK_RALT; else if ( parts[i].equals( "LMETA" ) ) modifierMask |= InputMapping.MODIFIER_MASK_LMETA; else if ( parts[i].equals( "RMETA" ) ) modifierMask |= InputMapping.MODIFIER_MASK_RMETA; } return ( modifierMask ); } public static String unparseModifierMask( int modifierMask ) { String stringValue = ""; if ( ( modifierMask & InputMapping.MODIFIER_MASK_SHIFT ) != 0 ) stringValue += "SHIFT+"; if ( ( modifierMask & InputMapping.MODIFIER_MASK_CTRL ) != 0 ) stringValue += "CTRL+"; if ( ( modifierMask & InputMapping.MODIFIER_MASK_LALT ) != 0 ) stringValue += "LALT+"; if ( ( modifierMask & InputMapping.MODIFIER_MASK_RALT ) != 0 ) stringValue += "RALT+"; if ( ( modifierMask & InputMapping.MODIFIER_MASK_LMETA ) != 0 ) stringValue += "LMETA+"; if ( ( modifierMask & InputMapping.MODIFIER_MASK_RMETA ) != 0 ) stringValue += "RMETA+"; return ( stringValue ); } public static String getComponentNameForTable( InputMapping mapping ) { String stringValue = ""; String[] tmp = mapping.getDeviceComponent().split( "::" ); int v0 = 0; if ( tmp.length > 1 ) { stringValue += tmp[0] + "::"; v0 = 1; } stringValue += unparseModifierMask( mapping.getModifierMask() ); for ( int i = v0; i < tmp.length; i++ ) stringValue += tmp[i]; return ( stringValue ); } public static Object[] parseMapping( int lineNr, String key, String value, InputDeviceManager devManager ) { key = key.trim(); value = value.trim(); int keyCode = -1; int modifierMask = 0; int hitTimes = 1; String[] keyParts = key.split( "::" ); String device = keyParts[0]; String component = keyParts[1]; if ( device.equals( "Keyboard" ) ) { String[] parts2 = component.split( "\\+" ); if ( parts2.length > 1 ) { modifierMask = parseModifierMask( parts2 ); component = parts2[parts2.length - 1]; } String[] parts3 = component.split( "," ); if ( parts3.length > 1 ) { component = parts3[0]; hitTimes = Integer.parseInt( parts3[1] ); } keyCode = devManager.getKeyIndexByEnglishName( parts3[0] ); if ( keyCode < 0 ) keyCode = devManager.getKeyIndex( parts3[0] ); if ( keyCode < 0 ) { RFDHLog.exception( " WARNING: The Keyboard doesn't have a key called \"" + parts3[0] + "\". Skipping mapping in line #" + lineNr + "." ); return ( null ); } component = devManager.getKeyName( keyCode ); } else if ( device.equals( "Mouse" ) ) { } else // Joystick { int jindex = devManager.getJoystickIndex( device ); if ( jindex < 0 ) { RFDHLog.exception( " WARNING: Joystick \"" + device + "\" not found. Skipping mapping in line #" + lineNr ); return ( null ); } device = devManager.getJoystickName( jindex ); int bindex; String[] parts3 = component.split( "," ); if ( parts3.length > 1 ) { bindex = devManager.getJoystickButtonIndex( jindex, parts3[0] ); if ( bindex < 0 ) { RFDHLog.exception( " WARNING: Joystick \"" + device + "\" doesn't have a button called \"" + parts3[0] + "\". Skipping mapping in line #" + lineNr + "." ); return ( null ); } hitTimes = Integer.parseInt( parts3[1] ); } else { bindex = devManager.getJoystickButtonIndex( jindex, component ); if ( bindex < 0 ) { RFDHLog.exception( " WARNING: Joystick \"" + device + "\" doesn't have a button called \"" + component + "\". Skipping mapping in line #" + lineNr + "." ); return ( null ); } } component = devManager.getJoystickButtonName( jindex, bindex ); } String[] valueParts = value.split( "::", 2 ); if ( valueParts.length < 2 ) { RFDHLog.exception( " WARNING: Illegal mapping at line #" + lineNr + ". Skipping." ); return ( null ); } String widgetName = valueParts[0]; String actionName; if ( valueParts.length == 1 ) actionName = null; else actionName = valueParts[1]; if ( ( actionName != null ) && ( KnownInputActions.get( actionName ) == null ) ) { RFDHLog.exception( " WARNING: No InputAction with the name \"" + actionName + "\" found. Skipping line #" + lineNr + "." ); return ( null ); } InputMapping mapping = new InputMapping( widgetName, KnownInputActions.get( actionName ), device + "::" + component, keyCode, modifierMask, hitTimes ); return ( new Object[] { mapping, getComponentNameForTable( mapping ) } ); } public InputMappings loadMappings( final GameFileSystem fileSystem, final InputDeviceManager devManager ) { try { File configFile = new File( fileSystem.getConfigFolder(), CONFIG_FILE_NAME ); final List<Object[]> rawBindings = new ArrayList<Object[]>(); String lastDevice = null; if ( !configFile.exists() || !configFile.canRead() ) { RFDHLog.exception( " WARNING: No readable " + CONFIG_FILE_NAME + " config file found in the config folder." ); numDevices = 0; numComponents = null; } else { new AbstractIniParser() { @Override protected boolean onSettingParsed( int lineNr, String group, String key, String value, String comment ) throws ParsingException { Object[] tmp = parseMapping( lineNr, key, value, devManager ); if ( tmp == null ) return ( true ); InputMapping mapping = (InputMapping)tmp[0]; String device = mapping.getDeviceComponent().split( "::" )[0]; String component = mapping.getDeviceComponent().split( "::" )[1]; String widgetName = mapping.getWidgetName(); String actionName = ( mapping.getAction() == null ) ? null : mapping.getAction().getName(); int keyCode = mapping.getKeyCode(); int modifierMask = mapping.getModifierMask(); int hitTimes = mapping.getHitTimes(); rawBindings.add( new Object[] { device, component, widgetName, actionName, keyCode, modifierMask, hitTimes } ); return ( true ); } @Override protected void onParsingFinished() { } }.parse( configFile ); Collections.sort( rawBindings, new Comparator<Object[]>() { @Override public int compare( Object[] o1, Object[] o2 ) { int cmp = ( (String)o1[0] ).compareTo( ( (String)o2[0] ) ); if ( cmp != 0 ) return ( cmp ); cmp = ( (String)o1[1] ).compareTo( (String)o2[1] ); if ( cmp != 0 ) return ( cmp ); return ( 0 ); } } ); numDevices = 0; numComponents = new int[ 256 ]; int maxNumComponents = 0; int nc = 0; for ( int i = 0; i < rawBindings.size(); i++ ) { String device = (String)rawBindings.get( i )[0]; if ( !device.equals( lastDevice ) ) { if ( numDevices == 1 ) { numComponents[0] = nc; if ( numComponents[0] > maxNumComponents ) maxNumComponents = numComponents[0]; } else if ( numDevices > 1 ) { numComponents[numDevices - 1] = nc - numComponents[numDevices - 2]; if ( numComponents[numDevices - 1] > maxNumComponents ) maxNumComponents = numComponents[numDevices - 1]; } numDevices++; lastDevice = device; } nc++; } if ( numDevices == 1 ) { numComponents[0] = nc; if ( numComponents[0] > maxNumComponents ) maxNumComponents = numComponents[0]; } else if ( numDevices > 1 ) { numComponents[numDevices - 1] = nc - numComponents[numDevices - 2]; if ( numComponents[numDevices - 1] > maxNumComponents ) maxNumComponents = numComponents[numDevices - 1]; } int[] tmp = new int[ numDevices ]; System.arraycopy( numComponents, 0, tmp, 0, numDevices ); numComponents = tmp; } // { numDevices[1], deviceType_0[1], deviceIndex_0[1], arrayOffset_0[2], numComponents_0[1], ... , deviceType_n[1], deviceIndex_n[1], arrayOffset_n[2], numComponents_n[1], componentIndex_0_0[2], ..., componentIndex_0_n[2], state_0_0[1], ..., state_0_n[1], ...... , componentIndex_n_0[2], ..., componentIndex_n_n[2], state_n_0[1], ..., state_n_n[1] buffer = ByteBuffer.allocateDirect( 1 + ( 1 + 1 + 2 + 1 ) * numDevices + 3 * rawBindings.size() ).order( ByteOrder.nativeOrder() ); buffer.position( 0 ); buffer.put( (byte)numDevices ); bufferOffsets = new short[ numDevices ]; lastDevice = null; int deviceIndex = 0; short arrayOffset = (short)( 1 + ( 1 + 1 + 2 + 1 ) * numDevices ); for ( int i = 0; i < rawBindings.size(); i++ ) { String device = (String)rawBindings.get( i )[0]; if ( !device.equals( lastDevice ) ) { if ( device.equalsIgnoreCase( "Keyboard" ) ) { buffer.put( (byte)1 ); buffer.put( (byte)0 ); } else if ( device.equalsIgnoreCase( "Mouse" ) ) { buffer.put( (byte)2 ); buffer.put( (byte)0 ); } else // Joystick { buffer.put( (byte)3 ); buffer.put( (byte)devManager.getJoystickIndex( device ) ); } bufferOffsets[deviceIndex] = arrayOffset; buffer.putShort( arrayOffset ); buffer.put( (byte)numComponents[deviceIndex] ); deviceIndex++; lastDevice = device; } arrayOffset += 3; } mappings = new InputMapping[ rawBindings.size() ]; //pluginEnabledBufferOffset = -1; lastDevice = null; deviceIndex = -1; int c = 0; for ( int i = 0; i < rawBindings.size(); i++ ) { String device = (String)rawBindings.get( i )[0]; String component = (String)rawBindings.get( i )[1]; String widgetName = (String)rawBindings.get( i )[2]; String actionName = (String)rawBindings.get( i )[3]; Integer keyCode = (Integer)rawBindings.get( i )[4]; Integer modifierMask = (Integer)rawBindings.get( i )[5]; Integer hitTimes = (Integer)rawBindings.get( i )[6]; if ( !device.equals( lastDevice ) ) { deviceIndex++; c = 0; lastDevice = device; } short componentIndex = 0; if ( device.equalsIgnoreCase( "Keyboard" ) ) { //componentIndex = (short)devManager.getKeyIndex( component ); componentIndex = (short)keyCode.intValue(); } else if ( device.equalsIgnoreCase( "Mouse" ) ) { // TODO } else // Joystick { int ji = devManager.getJoystickIndex( device ); componentIndex = (short)devManager.getJoystickButtonIndex( ji, component ); } //Logger.log( bufferOffsets[deviceIndex] + ", " + c + ", " + buffer.capacity() + ", " + buffer.limit() + ", " + ( bufferOffsets[deviceIndex] + ( c * 2 ) ) ); buffer.putShort( bufferOffsets[deviceIndex] + ( c * 2 ), componentIndex ); /* if ( actionName.equalsIgnoreCase( KnownInputActions.TogglePlugin.getName() ) ) { pluginEnabledBufferOffset = (short)( bufferOffsets[deviceIndex] + ( numComponents[deviceIndex] & 0xFF ) * 2 + c ); } */ mappings[i] = new InputMapping( widgetName, KnownInputActions.get( actionName ), device + "::" + component, keyCode, modifierMask, hitTimes ); RFDHLog.println( " Bound \"" + mappings[i].getDeviceComponent() + "\" to \"" + widgetName + "::" + actionName + "\"" ); c++; } states0 = new byte[ numDevices ][]; states1 = new byte[ numDevices ][]; for ( int i = 0; i < numDevices; i++ ) { states0[i] = new byte[ numComponents[i] ]; states1[i] = new byte[ numComponents[i] ]; } } catch ( Throwable t ) { RFDHLog.exception( t ); } return ( new InputMappings( mappings ) ); } private InputAction lastHitAction = null; private long firstActionHitTime = -1L; private int hitCount = 0; /** * @param eventsManager * @param widgetsManager the manager to fire widget events on * @param gameData the live game data * @param isEditorMode editor mode? (certainly false) * @param modifierMask the key modifier mask * * @return -1 if plugin got disabled, 0 if plugin was and is disabled, 1 if plugin was and is enabled., 2 if plugin got enabled. */ public int update( GameEventsManager eventsManager, WidgetsDrawingManager widgetsManager, LiveGameData gameData, boolean isEditorMode, int modifierMask ) { if ( !gameData.getProfileInfo().isValid() ) { return ( 0 ); } boolean wasPluginEnabled = isPluginEnabled; long when = gameData.getScoringInfo().getSessionNanos(); int k = 0; for ( int i = 0; i < numDevices; i++ ) { System.arraycopy( states1[i], 0, states0[i], 0, states1[i].length ); buffer.position( bufferOffsets[i] + 2 * numComponents[i] ); buffer.get( states1[i] ); buffer.position( 0 ); for ( int j = 0; j < states1[i].length; j++, k++ ) { boolean oldState = ( states0[i][j] != 0 ); boolean state = ( states1[i][j] != 0 ); if ( state != oldState ) { InputMapping mapping = mappings[k]; InputAction action = mapping.getAction(); if ( action.acceptsState( state ) && ( ( modifierMask & mapping.getModifierMask() ) == mapping.getModifierMask() ) ) { if ( action != lastHitAction ) { lastHitAction = action; firstActionHitTime = when; hitCount = 1; } else if ( when - firstActionHitTime < 1000000000L ) { hitCount++; } else { firstActionHitTime = when; hitCount = 1; } if ( hitCount >= mapping.getHitTimes() ) { hitCount = 0; lastHitAction = null; if ( action == KnownInputActions.TogglePlugin ) { isPluginEnabled = !isPluginEnabled; } else if ( isPluginEnabled && rfDynHUD.isInRenderMode() && gameData.isInCockpit() ) { if ( action.getConsumer() != null ) { action.getConsumer().onBoundInputStateChanged( action, state, modifierMask, when, gameData, isEditorMode ); } else if ( action == KnownInputActions.ToggleFixedViewedVehicle ) { //if ( gameData.getScoringInfo().isInCockpit() ) if ( gameData.getScoringInfo().isUpdatedInTimeScope() ) __GDPrivilegedAccess.toggleFixedViewedVSI( gameData.getScoringInfo() ); } else if ( action == KnownInputActions.IncBoost ) { //if ( gameData.getScoringInfo().isInCockpit() ) if ( gameData.getScoringInfo().isUpdatedInTimeScope() ) __GDPrivilegedAccess.incEngineBoostMapping( gameData.getTelemetryData(), gameData.getPhysics().getEngine() ); } else if ( action == KnownInputActions.DecBoost ) { //if ( gameData.getScoringInfo().isInCockpit() ) if ( gameData.getScoringInfo().isUpdatedInTimeScope() ) __GDPrivilegedAccess.decEngineBoostMapping( gameData.getTelemetryData(), gameData.getPhysics().getEngine() ); } else if ( action == KnownInputActions.TempBoost ) { //if ( gameData.getScoringInfo().isInCockpit() ) if ( gameData.getScoringInfo().isUpdatedInTimeScope() ) __GDPrivilegedAccess.setTempBoostFlag( gameData.getTelemetryData(), state ); } else if ( isPluginEnabled && action.isWidgetAction() ) { eventsManager.fireOnInputStateChanged( mapping, state, modifierMask, when, isEditorMode ); } } } } } } } if ( isPluginEnabled ) { if ( wasPluginEnabled ) return ( 1 ); return ( 2 ); } if ( wasPluginEnabled ) return ( -1 ); return ( 0 ); } public InputMappingsManager( RFDynHUD rfDynHUD ) { this.rfDynHUD = rfDynHUD; } }