[RFC] Single-file multi-axis

So I finally came around to reading this topic and had the exact same thought reading half-way through. The format y’all have specified and agreed upon seems to be the basic core structure, that is both backward-compatible and incorporating the main objective of multi-axis-scripts in one file.

Everything else, which would require both changes on the scripting as well as parser/player software side can be done imo with additional and optional metadata or sub-properties (e.g. UTC starting time, axis/track absolute dimensions like max twist angle or standard range scaling parameters for estim and in fact all axes), which are ignored by older or limited players and their parsers should not fail doing so (which I can’t fully assess of course).

1 Like

@Khrull @Yoooi @diglet what script metadata do you actually use? (if any)

Only actions, metadata.bookmarks and metadata.chapters, everything else is either from fleshlight launch days or does not really have standardized format/usage.
But I always planned to display all remaining properties from metadata in some sort of popup in MFP.

Funscript 2.0 pre-final draft

Valid funscripts MUST have the following format

type timeStamp = string // "hh:mm:ss.uuu"
type axis = string // implementation defined
type url = `http${string}`

interface Action {
  at: number // 0-1e9 int
  pos: number // 0-100 int
}
interface Channel {
  // sorted by .at
  // continuous (L/R) axes may not have two points with same .at
  // other can have two but not more (for steps)
  actions: Action[]
}
interface Script extends Channel {
  version: "2.0"
  metadata?: Partial<Metadata>
  // main axis action. 
  // main axis is based on extension prefix or implementation defined
  actions: Action[]
  channels?: {
    // channels may override main axis (implementation defined)
    [k in axis]?: Channel
  }
}
interface Metadata {
  title: string            // auto: topic title
  creator: string          // auto: topic OP
  tags: string[]           // auto: tags
  duration: number         // float, in seconds
  // durationTime is human-readable fallback
  durationTime: timeStamp  // auto: inferred from duration
  license: 'Paid' | 'Free' // auto: inferred from topic category
  video_url: url           // auto: inferred from a subset of links (ph, r34)
  script_url: url          // auto: topic url
  notes: string            // auto: notes section from topic (maybe)

  chapters: Chapter[]
  bookmarks: Bookmark[]
}
interface Chapter {
  startTime: timeStamp
  endTime: timeStamp
  name: string
}
interface Bookmark {
  time: timeStamp
  name: string | ""
}

Minimal script:

{
  "version": "2.0",
  "actions": [{ "at": 0, "pos": 50 }]
}

More complete script:

{
  "version": "2.0",
  "metadata": {
    "title": "Example Script",
    "creator": "John Doe",
    "tags": [
      "non-vr",
      "animation"
    ],
    "duration": 180.0017,
    "durationTime": "00:03:00.017"
  },
  "actions":[{"at":0,"pos":50}],
  "channels": {
    "random_text": {
      "actions":[{"at":0,"pos":50}]
    },
    "roll": {
      "actions":[{"at":1000,"pos":75}]
    },
    "vibe": {
      "actions":[{"at":2000,"pos":25}]
    }
  }
}

It is recommended to remove whitespace in actions array but keep it elsewhere.

formatted = JSON.stringify(s, null, 2)
  .replace(/(?<="actions":)\s*\[[^\]]+\]/g, 
    s => JSON.stringify(JSON.parse(s))
  )

A script with extension prefix .example.funscript is considered to be equivalent to a script with no extension prefix with actions moved into a single channel matching the prefix



Notes:

  • you may ignore durationTime, I’ll provide it in ES merged scripts but mostly for human-readable usage (to see what video duration they are looking for)
  • all channels with unknown names may be ignored

Edits:

  • removed axis type limitations
  • fixed example using T-code (it was a bug)
  • added extension prefix definition
Should duration be integer
  • integer s seconds
  • float s.mmm with milliseconds
0 voters
Do you approve this pre-final format version?
  • Yes, I will implement it as it’s described above
  • No, it’s missing properties I need
  • No, the existing properties are not correct
0 voters

You say compatibility with existing software is not considered, but at the same time backwards compatibility with single-axis stroker devices is somehow important. Que?

The simplest solution is the zip file. The simplest solution is not .max with a bunch of third-party tools to convert them back into legacy format.

Probably because OFS doesn’t support it.

My stim hardware/software uses absolute units. So the axis 0-100 could be mapped to 500-1000 hertz or 2-20 milliseconds or 0-100 percent, depending on the axis.

As for naming, I was thinking to specify the ranges inside the configuration file instead of the filename. For roll, there would be one singular roll funscript and it would be mapped to -30..30 deg for device A and -60...60 for device B (these limits would be specified in the config file). It could also be utilized the other way around: specify that the script was crearted with -30...30 in mind so it will always be -30...30 even if the device supports much larger range of motion.

This would be most relevant for estim devices because most funscripts are created with 0-100hz in mind even though some devices support up to 300hz. This is a problem that exists currently, where users need to change their axis limits depending on which set of funscripts they are playing.

We could also do twist1 and twist2, and the config file would specify which one would need to be used for which device. For example, depending on whether the device support 360 deg rotation or not. I’m not convinced this functionality would be all that useful for stroker-type devices, which is why I mentioned the config file could be application-specific.

I don’t want to get too far ahead on the specification because it’s quite likely restim will be the only software that will support it. And that’s fine, because MFP doesn’t support estim devices.

And because it minimizes the impact on existing software/scripts. Almost no code needs to be added in Restim, MFP and OFS to support zips because they either already support it or they support the unzipped folder (in OFS, you need to select the files manually). No special software needs to be installed by the end-user to unpack/repack .max files for legacy software.

The .max format requires two separate funscript parsers. This is not a big deal if .max offers considerable advantages over .zip… but I don’t see that.

L0 / L1 / R0 /… are implementation details of the specific device / protocol and SHOULD NOT be part of the funscript standard. There is no guarantee that a single funscript maps to exactly one device axis. There is no guarantee different devices that support the T-Code protocol and have the same capabilities have the same axis identifiers. There is no guarantee two-letter axis identifiers make any sense for devices that do not support the T-Code protocol.

I don’t use any script metadata because they’re either not useful for my purpose or are not properly documented.

2 Likes

XTP reads the metadata a group of us decided on a couple years ago. Ive yet to do anything with it though. I do read in the inverse property to function but I doubt many use it.

This looks right except for the TCode channels. Not sure why that keeps sneaking in there…

Leave out all the code specific implementations from now on. This thread is about funscript not parser implementations.

As far as track names I say we stick with the current multi file system names.

  • stroke
  • surge
  • sway
  • roll
  • pitch
  • twist
  • suck
  • vib
  • vib2-(n)
  • lube

TCode has multiple suck channels and makes things complicated. The above are already established with multi track devices.

The issue with zip is that many places block uploading of archives and that the compatibility is low, while a single file funscript with multiple tracks will work in anything currently able to read funscripts. Tho I guess the compatibility will be also low because it would have to be first implemented in XTP/MFP etc.
If you want most compatibility then the SLR format is supported for a long time in MFP.

Only downside of single file is you cant easily β€œupack” it to multiple files. But the idea is to automatically pack funscripts and allow the users to choose which files to download on eroscripts.

It seems that there are only 4 scripts in the whole eroscripts that have invert set to true, according to this:

In the spec I would not specify that the tracks need to stick with the current know names. Those names are sort of tied to TCode, so this funscript format would then be indirectly tied to TCode.

It needs to be specified in generic way.
video.name.funscript when merged produces:

{
    ...
    "tracks": {
        "name": {
           // video.name.funscript contents
        }
    }
}

L0 / L1 / R0 /… are implementation details of the specific device / protocol and SHOULD NOT be part of the funscript standard
it’s mapping where values are unused, copypasted from elsewhere
Will replace with plain keys I suppose

Well, it was meant as β€œsingle-axis funscripts should be supported too”, should reword as β€œsingle-axes funscripts are a channel inferred by extension prefix” same as it works now with multiple files

Hmm okay makes sense

v2 is an extension of v1, so the parser seems the same for me
Channel interface is just a stripped funscript file

tnx for mentioning, will clean pp all tcode remains

Edit: removed tcode, fixed example, added required extension prefix support
Tell me what’s wrong this time

Technicaly there shouldnt be a main, since that is device dependant. Which main is taken is decided at playback level.

Lets say you only have a venus2000 like machine, but the script has linear, twist, and vibration. Linear makes no sense in this case. So it shouldnt default to main. Since this one is speed dependant, it gets a script that is usualy more similar to vibration scripts.

So you can choose to map this to vibration or linear yourself. If it would automaticly take main, it might do the wrong thing. Otherwise it doesnt do anything unless explicitly chosen. This is important for safety reasons since unexpected behaviour is dangerous even if the device itself should already have taken care of safety.

Main is only needed if the format is backward compatible (ie. most of the scriptplayers can use this file without requiring any update). But if the script cannot be understood by most older programs, main must be excluded from the standard. If targeting a legacy device the playback tool does the mapping for you.

Dont define a main in a standard that doesnt need one, especialy if it can result in counter productive behaviour. Quick conversions of old scripts to this format is undesired! If its a legacy script, keep the old format.
So in this case, linear/linear1 is what should be used instead. That does define which type of device is targeted. Dont promote a main axis when a format is ment for multi axis at all.

So i would suggest that the moment any of the channels are used, only the channels are used. So whichever legacy main channel there was, its effectively discarded. However, to avoid duplicate data, we can still use it if we apply a sort of mapping system in the script.

So to use your example script, i propose this addition:

  },
  "actions":[{"at":0,"pos":50}],
  "channels": {
    "linear": {
      "channel":"main"
    },
    "random_text": {
      "actions":[{"at":0,"pos":50}]
    },

This then allows all channels to load in another channel, and while it might be ugly that 2 channels perform the same thing, It sometimes still works fine.

Or as alternative, we define the target of the main axis:

"duration": 180.0017,
    "durationTime": "00:03:00.017"
  },
  "actions":[{"at":0,"pos":50}],
  "channel": "linear"
  "channels": {
    "random_text": {

Which then at least prevents someone from mapping all channels to just a single one (which for multi axis is otherwise ugly.

1 Like

I understand that argument but it does not sound very convincing.

The .zip issue could easily be avoided by using a different extension. .docx works this way

Integrating software into eroscripts that assembles the files before download sounds like a bad idea to me. Not only is it much more complex than an archive file, it would force future funscript sharing websites to implement similar functionality.

Is there any convincing reason we are creating our own multiple-funscripts-in-one-json standard when the SLR format already exists?


I do feel the backwards compatibility issue is being misunderstood. The format, let’s call it the SLR format, is backwards compatible in the sense that it will work (to some extent) with older single-axis software/strokers. Such as Heresphere + handy and DeoVR + handy.

It is also β€˜backwards compatible’ with existing editing software such as OFS and various editing scripts/tools made over the years. However, loading any of the proposed multi-axis formats will not display the full information, so users are forced to upgrade all their software, some of which are no longer being maintained, if they want to do anything multi-axis.

So while the new format is backwards compatible with single-axis devices and software, it is not backwards compatible with multi-axis devices and software. The zip approach mostly is because everyone understands unzip and while an update to OFS would be nice, it is not required.


For any future funscript editing software, it’s important the software does not strip information that is not understood by the editing software.

I wouldn’t say these are tied to TCode in any way.
Some of them are nautical terms some of them are generic devices that could be on ANY device. TCode doesnt have these names at all.

In fact we made these name FOR the og multi funscript format. Had nothing to do with TCode. We just wanted a file to convert to tcode.

The names describe what the device does. Not what the device is nor what protocol it uses.

Because what they have is bad and it appears most people here on this thread agree.

Good insight.

Perhaps we can make stroke a reserved axis name, and whenever there is a β€˜default’ axis, it will be shown as stroke in the UI.

By omitting the β€œmain” actions array you’ve altered the funscript format. If youre going to do that then it shouldnt be called funscript any more to avoid confusion with current funscript players.

For reference these are the axis names currently used by Restim:

alpha, beta, gamma
volume
(carrier) frequency, pulse_frequency, pulse_width, pulse_interval_random, pulse_rise_time

Rarely used:
vib1_frequency, vib1_strength, vib1_left_right_bias, vib1_up_down_bias, vib1_random, also vib2

Planned / possible future use:
e1 up to e4, intensity, subjective_frequency

1 Like

One thing is for sure about this extended format. Existing players will need to add support for it if they want but its important we don’t break them.

If we want a new format then we should be creating a new format and NOT piggy backing on the funscript name.

Its already implemented in eroscripts, it merges multiple funscripts from a post into the SLR format.

I personally don’t mind using SLR since its already implemented in MFP for 2+ years.
But it uses TCode names for the axes/tracks so its just the naming that is meh. Otherwise it is pretty similar to the tracks format discussed earlier.

There is no need in reserve anything.

The format is very simple.
root actions you treat as if you are reading video.funscript
tracks.pitch you treat as if you are reading video.pitch.funscript
tracks.foo you treat as if you are reading video.foo.funscript
etc.

The script player defines what axis/devices each of those β€œfiles” is mapped to.

Well we decided on those names when implementing the first multi axis script players, to map those names to TCode axes.
So now if there is an update to TCode with a new axis then we will also have a new TCode β†’ funscript name mapping, hence if you force the format to only use known funscript names then it is indirectly tied to TCode imo.

I’m just saying that the format should be defined in generic terms, so instead of predefined known track names, it works for anything, like for example restim.