Skip to content

Commit 4c80dfc

Browse files
committed
Add raw TCode transport for Intiface exp.2
1 parent 8d9496e commit 4c80dfc

8 files changed

Lines changed: 385 additions & 187 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![Settings](docs/screenshots/setting.png) | ![macOS](docs/screenshots/macos.png) |
3838

39-
## Experimental v0.1.5-exp.1
39+
## Experimental v0.1.5-exp.2
4040

41-
The `v0.1.5-exp.1` prerelease adds experimental `Intiface / Buttplug` multi-axis support for compatible devices exposed by Intiface. This includes FUNSR-style SR1 / SR6 / PRO setups when they are detected correctly by Intiface.
41+
The `v0.1.5-exp.2` prerelease adds experimental `Intiface / Buttplug` multi-axis support for compatible devices exposed by Intiface. This includes FUNSR-style SR1 / SR6 / PRO setups when they are detected correctly by Intiface.
4242

4343
| Experimental v0.1.5 Preview |
4444
|:-:|
4545
| ![Experimental v0.1.5 Preview](docs/screenshots/preview_v015_exp1.png) |
4646

47-
- Download the prerelease: [ScriptPlayer+ v0.1.5-exp.1](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1)
47+
- Download the prerelease: [ScriptPlayer+ v0.1.5-exp.2](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2)
4848

4949
## What's New In v0.1.4
5050

@@ -83,7 +83,7 @@ The `v0.1.5-exp.1` prerelease adds experimental `Intiface / Buttplug` multi-axis
8383

8484
1. Download the latest `ScriptPlayerPlus-0.1.4-Windows-x64.zip` from [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)
8585
2. Extract and run `ScriptPlayerPlus.exe` — no installation required
86-
3. For the experimental Intiface build, download `ScriptPlayerPlus-0.1.5-exp.1-Windows-x64.zip` from [the v0.1.5-exp.1 prerelease](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1)
86+
3. For the experimental Intiface build, download `ScriptPlayerPlus-0.1.5-exp.2-Windows-x64.zip` from [the v0.1.5-exp.2 prerelease](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2)
8787

8888
### macOS
8989

docs/README_JA.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![設定](screenshots/setting.png) | ![macOS](screenshots/macos.png) |
3838

39-
## 実験版 v0.1.5-exp.1
39+
## 実験版 v0.1.5-exp.2
4040

41-
`v0.1.5-exp.1` のプレリリースでは、Intiface が認識した互換デバイス向けに実験的な `Intiface / Buttplug` マルチアクシス制御を追加しています。FUNSR 系の SR1 / SR6 / PRO も、Intiface で正しく検出されればこの経路でテストできます。
41+
`v0.1.5-exp.2` のプレリリースでは、Intiface が認識した互換デバイス向けに実験的な `Intiface / Buttplug` マルチアクシス制御を追加しています。FUNSR 系の SR1 / SR6 / PRO も、Intiface で正しく検出されればこの経路でテストできます。
4242

43-
| v0.1.5-exp.1 プレビュー |
43+
| v0.1.5-exp.2 プレビュー |
4444
|:-:|
45-
| ![v0.1.5-exp.1 プレビュー](screenshots/preview_v015_exp1.png) |
45+
| ![v0.1.5-exp.2 プレビュー](screenshots/preview_v015_exp1.png) |
4646

47-
- プレリリースのダウンロード: [ScriptPlayer+ v0.1.5-exp.1](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1)
47+
- プレリリースのダウンロード: [ScriptPlayer+ v0.1.5-exp.2](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2)
4848

4949
## v0.1.4 の追加内容
5050

@@ -83,7 +83,7 @@
8383

8484
1. [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)から最新の `ScriptPlayerPlus-0.1.4-Windows-x64.zip` をダウンロード
8585
2. 解凍して`ScriptPlayerPlus.exe`を実行 — インストール不要
86-
3. Intiface 実験ビルドは [v0.1.5-exp.1 プレリリース](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1) から `ScriptPlayerPlus-0.1.5-exp.1-Windows-x64.zip` をダウンロード
86+
3. Intiface 実験ビルドは [v0.1.5-exp.2 プレリリース](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2) から `ScriptPlayerPlus-0.1.5-exp.2-Windows-x64.zip` をダウンロード
8787

8888
### macOS
8989

docs/README_KO.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![설정](screenshots/setting_kor.png) | ![macOS](screenshots/macos.png) |
3838

39-
## 실험판 v0.1.5-exp.1
39+
## 실험판 v0.1.5-exp.2
4040

41-
`v0.1.5-exp.1` 프리릴리스는 Intiface가 인식한 호환 장치에 대해 실험적인 `Intiface / Buttplug` 다축 제어를 추가합니다. FUNSR 계열 SR1 / SR6 / PRO도 Intiface에서 정상 인식되면 이 경로로 테스트할 수 있습니다.
41+
`v0.1.5-exp.2` 프리릴리스는 Intiface가 인식한 호환 장치에 대해 실험적인 `Intiface / Buttplug` 다축 제어를 추가합니다. FUNSR 계열 SR1 / SR6 / PRO도 Intiface에서 정상 인식되면 이 경로로 테스트할 수 있습니다.
4242

43-
| v0.1.5-exp.1 미리보기 |
43+
| v0.1.5-exp.2 미리보기 |
4444
|:-:|
45-
| ![v0.1.5-exp.1 미리보기](screenshots/preview_v015_exp1.png) |
45+
| ![v0.1.5-exp.2 미리보기](screenshots/preview_v015_exp1.png) |
4646

47-
- 프리릴리스 다운로드: [ScriptPlayer+ v0.1.5-exp.1](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1)
47+
- 프리릴리스 다운로드: [ScriptPlayer+ v0.1.5-exp.2](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2)
4848

4949
## v0.1.4에서 추가된 내용
5050

@@ -83,7 +83,7 @@
8383

8484
1. [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)에서 최신 `ScriptPlayerPlus-0.1.4-Windows-x64.zip` 다운로드
8585
2. 압축 해제 후 `ScriptPlayerPlus.exe` 실행 — 설치 불필요
86-
3. Intiface 실험 빌드는 [v0.1.5-exp.1 프리릴리스](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1)에서 `ScriptPlayerPlus-0.1.5-exp.1-Windows-x64.zip` 다운로드
86+
3. Intiface 실험 빌드는 [v0.1.5-exp.2 프리릴리스](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2)에서 `ScriptPlayerPlus-0.1.5-exp.2-Windows-x64.zip` 다운로드
8787

8888
### macOS
8989

docs/README_ZH.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![设置](screenshots/setting.png) | ![macOS](screenshots/macos.png) |
3838

39-
## 实验版 v0.1.5-exp.1
39+
## 实验版 v0.1.5-exp.2
4040

41-
`v0.1.5-exp.1` 预发布为 Intiface 已识别的兼容设备加入了实验性的 `Intiface / Buttplug` 多轴控制。FUNSR 系列 SR1 / SR6 / PRO 只要能被 Intiface 正确识别,也可以通过这一路径进行测试。
41+
`v0.1.5-exp.2` 预发布为 Intiface 已识别的兼容设备加入了实验性的 `Intiface / Buttplug` 多轴控制。FUNSR 系列 SR1 / SR6 / PRO 只要能被 Intiface 正确识别,也可以通过这一路径进行测试。
4242

43-
| v0.1.5-exp.1 预览 |
43+
| v0.1.5-exp.2 预览 |
4444
|:-:|
45-
| ![v0.1.5-exp.1 预览](screenshots/preview_v015_exp1.png) |
45+
| ![v0.1.5-exp.2 预览](screenshots/preview_v015_exp1.png) |
4646

47-
- 下载预发布版本: [ScriptPlayer+ v0.1.5-exp.1](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1)
47+
- 下载预发布版本: [ScriptPlayer+ v0.1.5-exp.2](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2)
4848

4949
## v0.1.4 新增内容
5050

@@ -83,7 +83,7 @@
8383

8484
1.[Releases](https://github.com/sioaeko/scriptplayer-plus/releases) 下载最新的 `ScriptPlayerPlus-0.1.4-Windows-x64.zip`
8585
2. 解压后运行 `ScriptPlayerPlus.exe` — 无需安装
86-
3. Intiface 实验构建可从 [v0.1.5-exp.1 预发布](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.1) 下载 `ScriptPlayerPlus-0.1.5-exp.1-Windows-x64.zip`
86+
3. Intiface 实验构建可从 [v0.1.5-exp.2 预发布](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5-exp.2) 下载 `ScriptPlayerPlus-0.1.5-exp.2-Windows-x64.zip`
8787

8888
### macOS
8989

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scriptplayer-plus",
3-
"version": "0.1.5-exp.1",
3+
"version": "0.1.5-exp.2",
44
"description": "ScriptPlayer+ - Funscript video player with Handy and Intiface support",
55
"license": "PolyForm-Noncommercial-1.0.0",
66
"main": "dist-electron/main.js",

src/App.tsx

Lines changed: 12 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,22 @@ import {
1414
SubtitleFile,
1515
VideoFile,
1616
} from './types'
17-
import { getPositionAtTime, parseFunscript, transformFunscriptActions } from './services/funscript'
17+
import { parseFunscript, transformFunscriptActions } from './services/funscript'
1818
import { handyService, HandyUploadStatus } from './services/handy'
1919
import {
2020
ButtplugConnectionState,
2121
ButtplugDevice,
22-
ButtplugDeviceFrame,
23-
ButtplugFeature,
2422
buttplugService,
2523
} from './services/buttplug'
2624
import {
27-
getDefaultAxisValue,
25+
AxisActionMap,
26+
buildButtplugDeviceSignature,
27+
buildButtplugTransportCommand,
28+
buildFeatureMappingsForDevice,
29+
ButtplugFeatureMapping,
30+
getButtplugFeatureStorageKey,
31+
} from './services/buttplugDeviceControl'
32+
import {
2833
normalizeScriptBundle,
2934
SCRIPT_AXIS_IDS,
3035
} from './services/multiaxis'
@@ -43,11 +48,7 @@ const BUTTPLUG_FEATURE_MAPPINGS_STORAGE_KEY = 'scriptplayer-buttplug-feature-map
4348
const DEFAULT_BUTTPLUG_SERVER_URL = 'ws://127.0.0.1:12345'
4449

4550
type DeviceProvider = 'handy' | 'buttplug'
46-
type AxisActionMap = Partial<Record<ScriptAxisId, FunscriptAction[]>>
47-
type StoredButtplugFeatureMapping = {
48-
axisId: ScriptAxisId | ''
49-
invert: boolean
50-
}
51+
type StoredButtplugFeatureMapping = ButtplugFeatureMapping
5152

5253
function getMediaTypeFromPath(filePath: string): MediaType | null {
5354
const ext = '.' + (filePath.split('.').pop()?.toLowerCase() || '')
@@ -213,152 +214,6 @@ function getPrimaryAxis(bundle: FunscriptBundle | null): ScriptAxisId | null {
213214
return (Object.keys(bundle.scripts)[0] as ScriptAxisId | undefined) ?? null
214215
}
215216

216-
function buildButtplugDeviceSignature(device: ButtplugDevice): string {
217-
const featureSignature = device.features
218-
.map((feature) => `${feature.type}:${feature.index}:${feature.descriptor}:${feature.actuatorType || ''}`)
219-
.join('|')
220-
221-
return `${device.name}|${device.displayName}|${featureSignature}`
222-
}
223-
224-
function getButtplugFeatureStorageKey(deviceSignature: string, featureId: string): string {
225-
return `${deviceSignature}::${featureId}`
226-
}
227-
228-
function guessScriptAxisForFeature(feature: ButtplugFeature): ScriptAxisId | '' {
229-
const text = `${feature.descriptor} ${feature.actuatorType || ''}`.toLowerCase()
230-
231-
if (feature.type === 'linear') {
232-
if (text.includes('stroke') || text.includes('stroker') || text.includes('thrust')) return 'L0'
233-
if (text.includes('surge') || text.includes('forward') || text.includes('back')) return 'L1'
234-
if (text.includes('sway') || text.includes('left') || text.includes('right')) return 'L2'
235-
if (feature.index === 0) return 'L0'
236-
if (feature.index === 1) return 'L1'
237-
if (feature.index === 2) return 'L2'
238-
}
239-
240-
if (feature.type === 'rotate') {
241-
if (text.includes('twist')) return 'R0'
242-
if (text.includes('roll')) return 'R1'
243-
if (text.includes('pitch')) return 'R2'
244-
if (feature.index === 0) return 'R0'
245-
if (feature.index === 1) return 'R1'
246-
if (feature.index === 2) return 'R2'
247-
}
248-
249-
if (feature.type === 'scalar') {
250-
if (text.includes('vibe') || text.includes('vibrate')) return feature.index === 0 ? 'V0' : 'V1'
251-
if (text.includes('pump')) return 'V1'
252-
if (text.includes('valve')) return 'A0'
253-
if (text.includes('suck') || text.includes('suction')) return 'A1'
254-
if (text.includes('lube')) return 'A2'
255-
}
256-
257-
return ''
258-
}
259-
260-
function buildFeatureMappingsForDevice(
261-
device: ButtplugDevice | null,
262-
mappingStore: Record<string, StoredButtplugFeatureMapping>
263-
): Record<string, StoredButtplugFeatureMapping> {
264-
if (!device) return {}
265-
266-
const deviceSignature = buildButtplugDeviceSignature(device)
267-
const next: Record<string, StoredButtplugFeatureMapping> = {}
268-
269-
for (const feature of device.features) {
270-
const key = getButtplugFeatureStorageKey(deviceSignature, feature.id)
271-
const stored = mappingStore[key]
272-
next[feature.id] = {
273-
axisId: stored?.axisId ?? guessScriptAxisForFeature(feature),
274-
invert: stored?.invert ?? false,
275-
}
276-
}
277-
278-
return next
279-
}
280-
281-
function getAxisValueAtTime(axisId: ScriptAxisId, actionMap: AxisActionMap, timeMs: number): number {
282-
const actions = actionMap[axisId]
283-
if (!actions || actions.length === 0) {
284-
return getDefaultAxisValue(axisId)
285-
}
286-
287-
return getPositionAtTime(actions, timeMs) / 100
288-
}
289-
290-
function applyAxisMappingValue(axisId: ScriptAxisId, value: number, invert: boolean): number {
291-
const safeValue = Number.isFinite(value) ? value : getDefaultAxisValue(axisId)
292-
return invert ? 1 - safeValue : safeValue
293-
}
294-
295-
function buildButtplugFrame(
296-
device: ButtplugDevice,
297-
mappings: Record<string, StoredButtplugFeatureMapping>,
298-
actionMap: AxisActionMap,
299-
currentTimeMs: number,
300-
targetTimeMs: number,
301-
intervalMs: number
302-
): ButtplugDeviceFrame {
303-
const frame: ButtplugDeviceFrame = {
304-
linear: [],
305-
rotate: [],
306-
scalar: [],
307-
}
308-
309-
for (const feature of device.features) {
310-
const mapping = mappings[feature.id]
311-
const mappedAxisId = mapping?.axisId
312-
313-
if (!mappedAxisId) {
314-
if (feature.type === 'linear') {
315-
frame.linear?.push({ index: feature.index, position: 0.5, duration: intervalMs })
316-
} else if (feature.type === 'rotate') {
317-
frame.rotate?.push({ index: feature.index, speed: 0, clockwise: true })
318-
} else if (feature.type === 'scalar' && feature.actuatorType) {
319-
frame.scalar?.push({ index: feature.index, scalar: 0, actuatorType: feature.actuatorType })
320-
}
321-
continue
322-
}
323-
324-
const currentValue = applyAxisMappingValue(
325-
mappedAxisId,
326-
getAxisValueAtTime(mappedAxisId, actionMap, currentTimeMs),
327-
mapping?.invert ?? false
328-
)
329-
const targetValue = applyAxisMappingValue(
330-
mappedAxisId,
331-
getAxisValueAtTime(mappedAxisId, actionMap, targetTimeMs),
332-
mapping?.invert ?? false
333-
)
334-
335-
if (feature.type === 'linear') {
336-
frame.linear?.push({ index: feature.index, position: targetValue, duration: intervalMs })
337-
continue
338-
}
339-
340-
if (feature.type === 'rotate') {
341-
const delta = targetValue - currentValue
342-
frame.rotate?.push({
343-
index: feature.index,
344-
speed: Math.min(1, Math.abs(delta) * 1000 / Math.max(intervalMs, 1)),
345-
clockwise: delta >= 0,
346-
})
347-
continue
348-
}
349-
350-
if (feature.type === 'scalar' && feature.actuatorType) {
351-
frame.scalar?.push({ index: feature.index, scalar: targetValue, actuatorType: feature.actuatorType })
352-
}
353-
}
354-
355-
return {
356-
linear: frame.linear && frame.linear.length > 0 ? frame.linear : undefined,
357-
rotate: frame.rotate && frame.rotate.length > 0 ? frame.rotate : undefined,
358-
scalar: frame.scalar && frame.scalar.length > 0 ? frame.scalar : undefined,
359-
}
360-
}
361-
362217
export default function App() {
363218
const { locale, setLocale } = useTranslation()
364219
const [files, setFiles] = useState<VideoFile[]>([])
@@ -595,7 +450,7 @@ export default function App() {
595450
const effectivePlaybackRate = currentMedia.playbackRate > 0 ? currentMedia.playbackRate : playbackRate
596451
const currentTimeMs = currentMedia.currentTime * 1000 + (settings.timeOffset || 0)
597452
const targetTimeMs = currentTimeMs + intervalMs * effectivePlaybackRate
598-
const frame = buildButtplugFrame(
453+
const command = buildButtplugTransportCommand(
599454
selectedButtplugDevice,
600455
selectedButtplugFeatureMappings,
601456
runtimeAxisActions,
@@ -604,7 +459,7 @@ export default function App() {
604459
intervalMs
605460
)
606461

607-
await buttplugService.sendDeviceFrame(selectedButtplugDevice.index, frame)
462+
await buttplugService.sendDeviceFrame(selectedButtplugDevice.index, command.frame, { rawTCode: command.rawTCode })
608463

609464
if (runId !== buttplugStreamRunId.current) return
610465
buttplugStreamTimer.current = setTimeout(() => {

0 commit comments

Comments
 (0)