remove physical card scanning (enables sideloading)

This commit is contained in:
ppc
2026-01-30 22:16:58 +00:00
parent e4353fca97
commit 17603da953
11 changed files with 45 additions and 397 deletions
+10 -8
View File
@@ -77,15 +77,17 @@ const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
function AppContent() {
return (<Tab.Navigator tabBar={p => <AMUITabBar {...p}/>} screenOptions={{headerShown: false}}>
<Tab.Screen name="servers"
component={ServerManagerStack}
options={{tabBarIcon: ({color, size}) => (<Server color={color} size={size}/>)}}/>
return (
<Tab.Navigator tabBar={p => <AMUITabBar {...p}/>} screenOptions={{headerShown: false}}>
<Tab.Screen name="servers"
component={ServerManagerStack}
options={{tabBarIcon: ({color, size}) => (<Server color={color} size={size}/>)}}/>
<Tab.Screen name="cards"
component={CardManagerStack}
options={{tabBarIcon: ({color, size}) => (<WalletCards color={color} size={size}/>)}}/>
</Tab.Navigator>)
<Tab.Screen name="cards"
component={CardManagerStack}
options={{tabBarIcon: ({color, size}) => (<WalletCards color={color} size={size}/>)}}/>
</Tab.Navigator>
)
}
export default function AppRoot() {
+2 -15
View File
@@ -10,25 +10,12 @@
"ios": {
"bundleIdentifier": "moe.ppc.amnet",
"supportsTablet": true,
"associatedDomains": [
"applinks:card.ppc.moe"
],
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
},
"UIDesignRequiresCompatibility": true,
"UIViewControllerBasedStatusBarAppearance": false,
"NFCReaderUsageDescription": "AMNet uses NFC to scan Physical IC Cards",
"com.apple.developer.nfc.readersession.felica.systemcodes": [
"0003",
"88b4"
]
},
"entitlements": {
"com.apple.developer.nfc.readersession.formats": [
"TAG"
]
"UIViewControllerBasedStatusBarAppearance": false
},
"config": {
"usesNonExemptEncryption": false
@@ -36,7 +23,7 @@
},
"android": {
"package": "moe.ppc.amnet",
"versionCode": 8,
"versionCode": 9,
"adaptiveIcon": {
"foregroundImage": "./assets/icon-android.png",
"backgroundColor": "#DED8FD"
+7 -67
View File
@@ -1,7 +1,6 @@
import {BSON} from "realm";
import NfcManager from "react-native-nfc-manager";
import {ActivityIndicator, Button, Text, useTheme} from "react-native-paper";
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
import {ActivityIndicator, Button, IconButton, Text, useTheme} from "react-native-paper";
import {Alert, Platform, RefreshControl, ScrollView, Share, StyleSheet, View} from "react-native";
import {
CircleCheck,
@@ -13,7 +12,6 @@ import {
WifiOff,
TvMinimalPlay,
Star,
SmartphoneNfc,
SquareArrowOutUpRight
} from "lucide-react-native";
@@ -26,7 +24,6 @@ import RealmContext from "../models/RealmContext";
import {AMServer} from "../models/AMServer";
import {AMCard} from "../models/AMCard";
import {felicaCardReadAsync, icCard0008Number, ICType} from "../lib/iccard";
import {
AMCardInRequest,
AMServerConnectionInfo,
@@ -82,16 +79,6 @@ export default function ServerCardIn({route, navigation}) {
}
}, [realm, server, navigation]);
// nfc support
const [nfcSupported, setNfcSupported] = useState<boolean | null>(Platform.select({android: false, default: null}));
useEffect(() => {
if (nfcSupported !== null) {
return;
}
NfcManager.isSupported().then(setNfcSupported);
}, []);
// server state
const [serverRefreshing, setServerRefreshing] = useState<boolean>(false);
const [serverReloadCounter, setServerReloadCounter] = useState<number>(0);
@@ -101,7 +88,6 @@ export default function ServerCardIn({route, navigation}) {
const [cardInCompleted, setCardInCompleted] = useState<boolean | null>(null);
const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]);
const serverDefaultCard = useMemo(() => {
if (server?.isValid() !== true || !server.gameId) {
return null;
@@ -211,38 +197,6 @@ export default function ServerCardIn({route, navigation}) {
return () => abortController.abort();
}, [server]);
const performPhysicalCardIn = useCallback(async () => {
const card = await felicaCardReadAsync();
if (!card) {
return;
}
const savedCard = cards.find(x => x.physicalCardData?.cardIDm === card.idm);
if (savedCard) {
setUserSelectedCard(savedCard);
cardInCallback(savedCard);
return;
}
setCardInCompleted(false);
const cardId = card.type !== ICType.Amusement ? icCard0008Number(card.idm) : card.accessCode;
const abortController = new AbortController();
performCardIn(server, {cardId, physicalCardIDm: card.idm}, abortController.signal).then(r => {
setCardInCompleted(r);
}).catch(() => {
setCardInCompleted(false);
}).finally(() => {
setTimeout(() => setCardInCompleted(null), 3000);
});
setTimeout(() => abortController.abort(), 5000);
return () => abortController.abort();
}, [server]);
const performRefresh = () => {
setServerRefreshing(true);
setServerReloadCounter(serverReloadCounter + 1);
@@ -262,8 +216,7 @@ export default function ServerCardIn({route, navigation}) {
}
return (
<ScrollView contentInsetAdjustmentBehavior="automatic"
refreshControl={<RefreshControl refreshing={serverRefreshing} onRefresh={performRefresh}/>}>
<ScrollView contentInsetAdjustmentBehavior="automatic" refreshControl={<RefreshControl refreshing={serverRefreshing} onRefresh={performRefresh}/>}>
<View style={styles.container}>
<StatusAlert server={server} connectionEstablished={serverConnectionSuccess}/>
<View style={styles.section}>
@@ -274,28 +227,16 @@ export default function ServerCardIn({route, navigation}) {
actionIcon={<CircleCheck color="green" size={24}/>}>
{serverDefaultCard?.code == selectedCard.code && <DefaultCardDetail/>}
</ICCard>
<View style={{display: "flex", flexDirection: "row", gap: 5}}>
<Button style={{flex: 1}}
<Button style={{flex: 1}}
textColor="white"
disabled={disableCardIn}
contentStyle={{flexDirection: 'row-reverse'}}
mode={serverConnectionSuccess ? "contained" : "outlined"}
buttonColor={cardInCompleted === false ? "red" : "green"}
onPress={() => cardInCallback(selectedCard)}
icon={() => <SquareArrowOutUpRight color={disableCardIn ? "gray" : "white"}
size={18}/>}>
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
</Button>
{nfcSupported === true && (
<IconButton
mode="outlined"
disabled={disableCardIn}
onPress={performPhysicalCardIn}
style={{borderRadius: 10, margin: 0, height: "100%", flex: 0.2}}
icon={() => <SmartphoneNfc
color={disableCardIn ? "gray" : theme.colors.onBackground} size={18}/>}/>
)}
</View>
icon={() => <SquareArrowOutUpRight color={disableCardIn ? "gray" : "white"} size={18}/>}>
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
</Button>
</>
: <View style={styles.emptyPlaceholderView}>
<EmptyStatePlaceholder message="No card selected" icon={<CreditCard color="gray"/>}/>
@@ -329,8 +270,7 @@ export default function ServerCardIn({route, navigation}) {
{serverDefaultCard?.code == card.code && <DefaultCardDetail/>}
</ICCard>))
: <View style={styles.emptyPlaceholderView}>
<EmptyStatePlaceholder message="No additional cards"
icon={<SmartphoneNfcIcon color="gray"/>}/>
<EmptyStatePlaceholder message="No additional cards" icon={<SmartphoneNfcIcon color="gray"/>}/>
</View>}
</View>)}
</View>
+3 -29
View File
@@ -1,15 +1,12 @@
import React from "react";
import {StatusBar} from "expo-status-bar";
import NfcManager from "react-native-nfc-manager";
import React, {useEffect, useState} from "react";
import {Platform, TouchableOpacity, View} from "react-native";
import {createNativeStackNavigator} from "@react-navigation/native-stack";
import {Card, Text, useTheme} from "react-native-paper";
import {ShieldBan, SmartphoneNfc, SquareAsterisk, SquarePlus} from "lucide-react-native";
import {SquareAsterisk, SquarePlus} from "lucide-react-native";
import ManualCardEntry from "./ManualCardEntry";
import PhysicalCardScan from "./PhyscialCardScan";
const Stack = createNativeStackNavigator();
@@ -18,7 +15,6 @@ function CardAddModal() {
<Stack.Navigator initialRouteName="Select" screenOptions={{headerTitleStyle: {fontWeight: "bold"}}}>
<Stack.Screen name="Select" component={CardAddMethodSelector} options={{headerShown: false}}/>
<Stack.Screen name="Scan" component={PhysicalCardScan} options={{title: "Add Physical Card"}}/>
<Stack.Screen name="Manual" component={ManualCardEntry} options={{title: "Add Card Manually"}}/>
</Stack.Navigator>
)
@@ -26,11 +22,6 @@ function CardAddModal() {
function CardAddMethodSelector({navigation}) {
const theme = useTheme();
const [nfcSupported, setNfcSupported] = useState<boolean | null>(null);
useEffect(() => {
NfcManager.isSupported().then(setNfcSupported);
}, []);
return (
<>
@@ -45,19 +36,6 @@ function CardAddMethodSelector({navigation}) {
</Text>
</View>
{nfcSupported !== null && (
<>
<MethodCard title="Scan Physical Card"
disabled={!nfcSupported}
color={nfcSupported ? "#03a9f4" : "gray"}
icon={nfcSupported ? <SmartphoneNfc/> : <ShieldBan/>}
description={nfcSupported ? "Amusement IC or Transport cards supported" : "NFC is not supported on this device"}
onPress={() => navigation.navigate('Scan')}/>
<View></View>
</>
)}
<MethodCard color={theme.colors.secondary}
icon={<SquareAsterisk/>}
title="Enter Details Manually"
@@ -106,8 +84,4 @@ function MethodCard(props: {
)
}
// the nfc library doesn't offer the same support for felica cards on android compared to ios...
export default Platform.select({
ios: CardAddModal,
default: ManualCardEntry
});
export default CardAddModal;
@@ -1,87 +0,0 @@
import {StatusBar} from "expo-status-bar";
import {useNavigation} from "@react-navigation/native";
import {Card, Text, useTheme} from "react-native-paper";
import {useCallback, useLayoutEffect, useState} from "react";
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
import RealmContext from "../../models/RealmContext";
import {useModalStyles} from "../../utils/ModalStyles";
import {AMCard} from "../../models/AMCard";
import {AMPhysicalCardData} from "../../models/AMPhysicalCardData";
import {felicaCardReadAsync, icCard0008Number, ICType} from "../../lib/iccard";
const {useRealm} = RealmContext;
export default function PhysicalCardScan() {
const realm = useRealm();
const theme = useTheme();
const navigation = useNavigation();
const modalStyles = useModalStyles();
const [cardName, setCardName] = useState<string>('');
const handleScan = useCallback(async () => {
const cardInfo = await felicaCardReadAsync();
if (!cardInfo) {
return;
}
realm.write(() => {
// check if the card has already been registered (convert a manually entered card to a physically scanned card)
const accessCode = cardInfo.type === ICType.Amusement ? cardInfo.accessCode : icCard0008Number(cardInfo.idm);
const cardEntity = realm.objects<AMCard>(AMCard.schema.name).find(x => x.code === accessCode) ?? realm.create<AMCard>(AMCard.schema.name, AMCard.generate(accessCode, cardName));
// set physical card props
cardEntity.name = cardName;
cardEntity.added = new Date();
cardEntity.physicalCardData ??= realm.objectForPrimaryKey<AMPhysicalCardData>(AMPhysicalCardData.schema.name, cardInfo.idm) ?? realm.create<AMPhysicalCardData>(AMPhysicalCardData.schema.name, {
cardIDm: cardInfo.idm,
cardType: cardInfo.type,
cardVendor: cardInfo.type === ICType.Amusement ? cardInfo.vendor : null
});
});
// dismiss the parent screen (this screen can only be accessed from modal -> stack)
navigation.getParent()?.goBack();
}, [realm, navigation, cardName]);
useLayoutEffect(() => {
const canSave = cardName.length;
const themeOptions = canSave ? modalStyles.headerText : {color: theme.colors.outline};
navigation.setOptions({
headerRight: () => <TouchableOpacity disabled={!canSave} onPress={() => handleScan()}>
<Text variant="bodyMedium" style={{...themeOptions}}>Scan</Text>
</TouchableOpacity>
})
}, [navigation, modalStyles, cardName]);
return (<>
{Platform.OS == 'ios' && <StatusBar style="light"/>}
<View style={modalStyles.rootLayout}>
<Card mode="contained">
<Card.Content>
<View style={modalStyles.formItemRow}>
<Text variant="bodyMedium" style={modalStyles.formItemName}>
Card Name
</Text>
<TextInput style={modalStyles.formItemInput}
autoFocus
numberOfLines={1}
value={cardName}
onChangeText={setCardName}
placeholder="Primary Card"
placeholderTextColor={theme.colors.outline}/>
</View>
</Card.Content>
</Card>
<Text variant="bodyMedium" style={modalStyles.formFootnote}>
Ensure you have either an Amusement IC or Transport card ready to scan.
After the scan has completed, the card will be saved to your device.
</Text>
</View>
</>)
}
-49
View File
@@ -1,49 +0,0 @@
import {Buffer} from "buffer";
const s_box_inv = [
"243cba36e385a4d0934373b9706ec9f1100e9b2c97e70b636c2920fe86f3e1f5f69fb616047b8fabb1392a1beb5ca8ac3811125f893e7dcaec53db6d1ed281789646fff9541c287a4fd3c0dcc16af2bccb57fd4ae4f0b2c79540625241e2ad49a6b51f02c8da92e5b0c564764821de0f45584cdfa784cfd5154e27806b7ffc44714722dd300ca13be037a03523903274bf8dc2ead650bbd9c483b43168558b5df47218b7eff7982d010361cc0a8e1300e814af095175a52f1d0d658acd66073d05d84be99d997c91d1b819c32b426988c679173a4d5ba25afb253fbdf8edcee687a326a92ebe94087e67608c9c5e6fb39e06fa82aeee5977d71a9a34d456aa33",
"f9817c00b93037d590516ef0b206fbcd39145ff81bc74a82708c921deac34ec812e6b610d32f95842542a572e58f55ef86a253aeed262047de786828e445cc35bc3ebb8ec0be34afa60964017b44f5f3b13fa4b33280c94f6f40bf214c74e1115eeb3d712e9d6275d41648771367ad6b5d0787f7a89a59768b9b1ed8eeaae9992dc597f1a783fccadcbab84be88917050ba06523d1ce3603d7e0f291df0e9c2c0d66d673585cb40a4dc23bd2b7e2ac33db6027bd56432404028d7d7ff6b5f4fddd5bec79c4221fa18854e71998947e31a329e35acb6ab0cf6d936c7a08a93c1cd06350c68538c14149ab61522a9fd9181a57462b69dafaff0f15963a8afe9e0c",
"11a29ac394a051015bf7d130ee1f06d57767d3c16ee76a1aa48e9f6566d60c2dc6e469b7565a5760fc791ee11652f007d0cdca78c98bb38827f6e0c084cf2fa304008040a7fa759d4b89462228373413ff20b4da08266c8ff25d7b735c4c9071418a9ebb12e8556d2a254aafbd9519a531baadd9c5a66b83c4b6a9ccf9a1b17adcc7db2e7e7f8d4e620f0339fdb8d418c2ec6102531c3b7dbc1d59f58c98f16f9385f82c74d85e4d97ab87820a21425f0d5833443adde9fb5047c8d7819be51b1754862beff429320e7c2315ebe63c35e296683f0b43cede9cbe4f457276b510e3df92d2a84891b9acbf49b2996470eaf33e6314aeedaa24fe3dcbb009383605",
"c4f02849554af5fd75af2069c843866bc9a8c6544cdd025be89b597734d7c0515ff37ed4f19081ce198a7833cd978cd66a4b8fa480dc1a1714c5078766ad9d85b2f88d98df3e1f3f645840ac6c5c085d53baffd2a92c25abe432389af9cbc29167d0e322292d9248b40e9905a36f0a15efd5104fd8f600e6462b31578394ae03eb8e1324a11e5ed126d9a7ca366e7137eeb1fa7c7606b823be21bc9ec1da7a3b62277bb0e56318827f0feda02a9cbf11bde0880d3a795256f7b6d309161b70b342602f1ca29f72124547bbe10b018b1ddeec0c4ee9cfcc9574f4a593c7db4dfb3565fee739b5ea96c304414484fc6d30e2f268b7897d733db95a50a63c61aa2e",
"1a599cadc8e41154ed370f3ae65f3c4bb81589b1e8da697791568bdb0624cf18f8b087df8c35cb86539da4664a7a710a4838ffdc8320ce9832f6d7af705e738a14721ec129790708e94346d02fde2a4f3d2c50b375fc0b64ae317b61a530a093d2bec255ba6cf36268fdac3b95491f6bb485f7a603ec6e9a81090c6aee9e4ef0ab2d7ea1e1bbc9bc41f2fa2e1bdd2734d36004b501d640f9d50247d9d8e08f2bfbb7c5eb5716e5788df400cd8239c696bdbfe336450e261d63e7843eb69b22a3f58e236dc3997d9097102580d44ce274ccb25c33c7ea05123f51a8fef11c42cad176286f92a767a919a2445baaef587f21c08865b95d4d0d5ac413527c9f9417",
"c72b82615bd09684d34a70a19b59339fc02014532917c50bc97b97020d3a1e7c3f6b52e8753df6e40c8a4fc35f2665733123284874aaa73609b1e291045122fc08a605a4f1121c19eb4037c2a0411dd4dc07438f47afd12e98ab01baf06668acf9e769b6cb8d78871503d5dfa31a9d6aea2f944e9e42d7b83892d8bbdedd9abcb04c79f4583ee98381ffe355fd5db2ef9c6d549960da3becfa11d6c42aed4bae13bfb9068be01f7f5aad90390ff3bd466c2cf2f8fed9e6720e89be5ec6a8cff5577e8cb7e1887ac81b18db6f35d216326463a58ef7b47695867de5a95c8500c1934dfb30a2b5253410773c67712d4445560a50b3cd6280ce214924caee276ecc",
"a5b06db251551fbf3e8ddd1992ea1bbd326529a48967b1906800da0da65954f210142f45e03b23fad750ac93e643010a5c78709846a97bf395072ad374b33fa36082394e3448c01cf6c291644d3cf89d359a94e37af0f96eb912b8042d02281385728087db2bf14f26a2e1497e9ccca7b6a0d09b3677ad8b6b4a031d058a064bafe531b5d4c60c66ba83fd090fae71b76fdc41e8178e407f6230ffa88425fb16ce3744ab991eeb183a47f75f81cfed582c6cfe9e57539720ca791a5a88f5699fe7d90ebe42df56e44c22aa730b15c5eefcc7d6cbcd8ce27621e9d1ecc87dd88f617c2ebcdeb475d2c4633da15e5d6a0824c927bbef33865bd5385211f4c396c1",
"345adb2c5957122b30c2a092bfedbc45de279b96d3e6c5ebd8244ba421cca8d6ce3cbab109e0d732664a831d19ca89670f420771e9bb445e8517c4ae9c3f6b78e76d02a7fe33e3d90a7deaf118877b6203795223003e25b9dd16683b1f90a62ac629918a9def1e8476ee4f39fb111b0b93ad49b705e14dcbe838d055f2f70d8b65cdfad49ec9814e14c826b6f9aaf580f86a98ab58f3b88eb5974372a2da64c1405c13b0e5bd089f1c7e8f06be6e501a2f37a1fc102e705304206348e2fd6f7a7561b3353acf5b888251d247a5a9746c31947c9ac0ec150e28012dc3f0b4e48ddcac3d5f86c79522f4b2a35446d1778c0cffaf565d36697f994cd56073df41f6",
"04da79631ebdeae30b652501cda992ed187557308903096adcc798a450911fb10c778566ca121c67c2e01783599dd5228256a89fc66048b311fca328fb06dd5dba297a4fe8fee910cbf3937b6c6954e744a2841d8dcefffa1a879074a1f814aabcc0cf31b4d9bfd85e262dd0e43f19d68c2fab3958726edf3be63cb66288de40b28f9b7c9543d19eac9cd423e10e4b5333462013341b97f7f1c3614a6f5a217f702e554105c5d776e52715ec425c4d78358befd2eeb5beae023ad35fc424f0f9514eeb000ffdaf3ec19a528681807ef62bccb97d68f2ad99a7072c7338b06bb78e71a0f43da60d37db0a473616966d322a5be24594f5a54cc88a4964bb08c9b8"
];
function rotate_right(data: Uint8Array, nBytes: number, nBits: number) {
let prior = data[nBytes - 1];
for (let i = 0; i < nBytes; i++) {
let current = data[i];
data[i] = (current >> nBits) | ((prior & ((1 << nBits) - 1)) << (8 - nBits));
prior = current;
}
}
const S_BOX_INV = s_box_inv.map(x => new Uint8Array(Buffer.from(x, 'hex')));
const N_TABLES = S_BOX_INV.length - 1;
const ITER_ADD = 5;
export function performDecrypt(data: Uint8Array) {
const spad = data.map(x => S_BOX_INV[N_TABLES][x]);
let count = (spad[15] >> 4) + 7;
let table = spad[15] + ITER_ADD * count;
for (let z = 0; z < count; z++) {
table -= ITER_ADD;
rotate_right(spad, 15, 5);
for (let i = 0; i < 15; i++) {
spad[i] = S_BOX_INV[table % N_TABLES][spad[i]];
}
}
return {
version: spad[0],
accessCodeBytes: spad.slice(6) // access code is a binary-coded decimal
};
}
-94
View File
@@ -1,8 +1,3 @@
import {Buffer} from "buffer";
import NfcManager, {NfcTech} from "react-native-nfc-manager";
import {performDecrypt} from "./iccard-crypto";
export enum ICType {
Amusement = 1,
Transport = 2
@@ -41,92 +36,3 @@ export function icCard0008Number(idm: string): string {
// 0008 is the numerical representation of the idm padded with zeros until it is 20 digits
return BigInt(`0x${idm}`).toString().padStart(20, '0');
}
/**
* Reads a FeliCa card, returning access code information.
*/
export async function felicaCardReadAsync(): Promise<ICCard | null> {
try {
await NfcManager.requestTechnology(NfcTech.FelicaIOS, {
isReaderModeEnabled: true,
invalidateAfterFirstRead: false,
alertMessage: "Hold an Amusement IC or Transport card to the top of your phone to scan it."
});
const tag = await NfcManager.getTag();
const idm = tag['idm'] as string;
switch (tag['systemCode'] as string) {
case "0003":
return {
type: ICType.Transport,
idm
};
case "88B4":
return await readAmusementICCardContents(idm);
default:
return null;
}
} catch (ex) {
return null;
} finally {
await NfcManager.cancelTechnologyRequest();
}
}
async function readAmusementICCardContents(idm: string): Promise<AmusementICCard | null> {
const idmBytes = Uint8Array.from(Buffer.from(idm, 'hex'));
const readCommand = [
0x06, // read
...idmBytes,
0x01, // service count (fixed)
0x0B, 0x00, // service code (000b, le)
0x02, // block count
0x80, 0x00, // S_PAD0 block
0x80, 0x82 // ID block
];
let response = await NfcManager.sendFelicaCommandIOS(readCommand);
const offset = response.indexOf(0x07); // look for 0x07 as for some reason the response may not start at response[0]
// response must have 44 bytes
if (offset === -1 || response.length - offset !== 44) {
return null;
} else if (offset !== 0) {
response = response.slice(offset);
}
// response format:
// 0 - read response (0x07)
// 1-8 - IDm
// 9 - status flag 1
// 10 - status flag 2
// 11 - number of blocks (0x02)
// 12-27 - S_PAD0 block
// 28-43 - ID block (DFC is 35-36)
if (response[9] !== 0x00 || response[11] !== 0x02) {
return null;
}
const dfcValue = response[37] | (response[36] << 8);
if (dfcValue in AmusementICVendor && dfcValue !== AmusementICVendor.Unknown) {
const accessCode = performDecrypt(new Uint8Array(response.slice(12, 28)));
return {
type: ICType.Amusement,
accessCode: [...accessCode.accessCodeBytes].map(x => x.toString(16).padStart(2, '0')).join(''),
vendor: dfcValue,
idm
};
}
return {
type: ICType.Amusement,
accessCode: icCard0008Number(idm),
vendor: AmusementICVendor.Unknown,
idm
};
}
+8 -24
View File
@@ -1,12 +1,12 @@
{
"name": "amnet-native",
"version": "1.3.2",
"version": "1.3.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "amnet-native",
"version": "1.3.2",
"version": "1.3.9",
"dependencies": {
"@babel/runtime": "^7.28.4",
"@expo-google-fonts/jetbrains-mono": "^0.4.1",
@@ -32,7 +32,6 @@
"react-native-context-menu-view": "^1.21.0",
"react-native-gesture-handler": "^2.30.0",
"react-native-get-random-values": "^2.0.0",
"react-native-nfc-manager": "^3.17.2",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~4.2.1",
"react-native-safe-area-context": "5.6.2",
@@ -1626,7 +1625,6 @@
"resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz",
"integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@expo/config-types": "^54.0.10",
"@expo/json-file": "~10.0.8",
@@ -7344,20 +7342,6 @@
"react-native": "*"
}
},
"node_modules/react-native-nfc-manager": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/react-native-nfc-manager/-/react-native-nfc-manager-3.17.2.tgz",
"integrity": "sha512-0NryP/Iw2hzw4MVH5KCngoRerNUrnRok6VfLrlFcFZRKyTQ7KTgpsdDxCB6cR33qYNyEDrWGBayfAI+ym5gt8Q==",
"license": "MIT",
"peerDependencies": {
"@expo/config-plugins": "*"
},
"peerDependenciesMeta": {
"@expo/config-plugins": {
"optional": true
}
}
},
"node_modules/react-native-paper": {
"version": "5.14.5",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.14.5.tgz",
@@ -8519,9 +8503,9 @@
}
},
"node_modules/tar": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@@ -8810,9 +8794,9 @@
}
},
"node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "amnet-native",
"version": "1.3.2",
"version": "1.3.9",
"private": true,
"scripts": {
"android": "expo run:android",
@@ -33,7 +33,6 @@
"react-native-context-menu-view": "^1.21.0",
"react-native-gesture-handler": "^2.30.0",
"react-native-get-random-values": "^2.0.0",
"react-native-nfc-manager": "^3.17.2",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~4.2.1",
"react-native-safe-area-context": "5.6.2",
+3 -3
View File
@@ -1253,9 +1253,9 @@
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lru-cache": {
+11 -19
View File
@@ -1,15 +1,19 @@
<div align="center">
# AMNet
[TestFlight](https://testflight.apple.com/join/kHbeJTsQ) • [Firebase](https://appdistribution.firebase.dev/i/6acc3151357deaa8) • [Website](http://card.ppc.moe) • [Server DLLs](https://gitea.tendokyu.moe/ppc/amnet/releases)
[Firebase](https://appdistribution.firebase.dev/i/6acc3151357deaa8) • [Website](http://card.ppc.moe) • [Server DLLs](https://gitea.tendokyu.moe/ppc/amnet/releases)
An Aime Card Switcher for segatools-based games.
An Amusement IC Card Switcher for segatools-based games.
</div>
## Overview
AMNet is designed to simulate the `DEVICE/aime.txt` file, allowing multiple users to log into a game using their phone.
This was originally intended for use with maimai and CHUNITHM, but should be compatible with any segatools-based game with Aime support.
AMNet is designed to acts as dynamic version of the `DEVICE/aime.txt` file, allowing multiple users to log into a game using their own mobile devices.
This was originally intended for use with maimai and CHUNITHM, but should be compatible with any segatools-based game with that uses Amusement IC for account management.
> [!WARNING]
> TestFlight is no longer available and must be sideloaded using AltStore or similar tooling for the forseeable future.
> See issue #1 for more details.
## Setup
1. Download the latest `amnet.dll` from the releases page
@@ -26,7 +30,7 @@ This was originally intended for use with maimai and CHUNITHM, but should be com
; serverName - the name of the server to show to users (defaults to the computer name if not set)
; serverAddress - the address to listen on, including the protocol (don't set/change unless you know what you're doing)
; enableKeyboardMode - whether to enable hold-enter-to-login mode. Reads the file set at aimePath for card number (or DEVICE/aime.txt if not set).
; useAimeDBForPhysicalCards - Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, set this to 1 to load the same account data/profile as you would get on a physical cab.
; useAimeDBForPhysicalCards - Whether codes from real cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, set this to 1 to load the same account data/profile as you would get on a physical cab.
gameId=SDEZ
;serverName=CHUNI-PENGUIN
;serverAddress=http://0.0.0.0:6070
@@ -36,18 +40,6 @@ This was originally intended for use with maimai and CHUNITHM, but should be com
4. Launch the game, and check the amdaemon console for server startup messages
5. Access the using one of the interfaces (see below), add a new server `http://<your-ip-address>:6070`, import your existing card (or create a new one) and start playing!
### Physical Cards
AMNet 1.3 adds support for scanning physical cards on iOS devices. If you own a physical Aime/Banapass/NESiCA/e-Amusement card, you can scan it using the app and use it in-game.
By changing the `useAimeDBForPhysicalCards` in segatools.ini, you can choose how the card is read on a per-game basis:
- `0` will send the 20-digit access code on the back of the card to the game and load the save data directly (default option).
- `1` - Send the card's IDm to the game and use AiMeDB to retrieve the 20-digit code assigned to the card.
Mode `1` makes the app function in the same way to a real card reader and should be used if you want the same access code to show when scanning the card on a cab.
You should also use this option if you're connecting to a hosted server (AquaNet/rinnet/etc).
Additionally, if `useAimeDBForPhysicalCards` is set to `1` and the game is failing to read the card, make sure an up-to-date version of segatools is being used.
## Interfaces
AMNet can be used on most platforms, see below for options available for each platform:
@@ -60,8 +52,8 @@ AMNet can be used on most platforms, see below for options available for each pl
</tr>
<tr>
<td>iOS</td>
<td><a href="https://testflight.apple.com/join/kHbeJTsQ" target="_blank">TestFlight</a></td>
<td>Aime/Banapass/NESiCA/e-Amusement IC cards supported</td>
<td><a href="https://amnet-dist.ppc.moe/amnet-1.3.2.ipa" target="_blank">.ipa download</a></td>
<td>*TestFlight is no longer available, sideload using AltStore or similar tooling</td>
</tr>
<tr>
<td rowspan="2">Android</td>