project-slippi / slippi-js

Parse slp files and compute stats
GNU Lesser General Public License v3.0
148 stars 79 forks source link

Using slippi-js in the browser? #64

Closed DustinAlandzes closed 3 years ago

DustinAlandzes commented 3 years ago

Hey! So I was trying to make a minimal example of parsing .slp file with slippi-js in the browser.

I understand that Buffer is a nodejs thing, but it seems possible with this library: https://github.com/feross/buffer

https://github.com/DustinAlandzes/parse-slp-stats-in-browser/blob/main/src/UploadScreen.tsx

    async function beforeUpload(file: File): Promise<File> {
        const reader = new FileReader();
        reader.readAsArrayBuffer(file)
        reader.onload = () => {
            if (reader.result) {
                const game = new SlippiGame(Buffer.from(reader.result));
                if (game) {
                    setFileNameToGameMapping(prevState => ({...prevState, [file.name]: game}))
                }
            }
        }
        return file
    }
    return <div>
        <Upload
            fileList={fileList}
            onChange={handleChange}
            action={'https://httpbin.org/post'}
            itemRender={itemRender}
            beforeUpload={beforeUpload}
            directory>
            <Button type="dashed">
                Click me to open the prompt, and choose your Slippi folder
            </Button>
            <Divider/>
        </Upload>
     </div>

For some reason it's working here: https://slp.spaceanimalz.com/

but when I tried on codesandbox I keep getting a "TypeError: argument should be Buffer"

https://codesandbox.io/s/wandering-dew-xtff2?file=/src/App.tsx

image

This is an example that doesn't use antd: https://codesandbox.io/s/vibrant-platform-4zy3o?file=/src/App.tsx

I believe this is the code path the exception happens on

export function getMetadata(slpFile: SlpFileType): MetadataType | null {
  if (slpFile.metadataLength <= 0) {
    // This will happen on a severed incomplete file
    // $FlowFixMe
    return null;
  }

  const buffer = new Uint8Array(slpFile.metadataLength);

  // it happens here in readRef
  readRef(slpFile.ref, buffer, 0, buffer.length, slpFile.metadataPosition);

  let metadata = null;
  try {
    metadata = decode(buffer);
  } catch (ex) {
    // Do nothing
    // console.log(ex);
  }

  // $FlowFixMe
  return metadata;
}

https://github.com/project-slippi/slippi-js/blob/master/src/utils/slpReader.ts#L71-L80

function readRef(ref: SlpRefType, buffer: Uint8Array, offset: number, length: number, position: number): number {
  switch (ref.source) {
    case SlpInputSource.FILE:
      return fs.readSync((ref as SlpFileSourceRef).fileDescriptor, buffer, offset, length, position);
    case SlpInputSource.BUFFER:
      // happens right here, during the buffer.copy
      return (ref as SlpBufferSourceRef).buffer.copy(buffer, offset, position, position + length);
    default:
      throw new Error("Source type not supported");
  }
}

I understand if this is out of scope for the project, but curious to see if anyone else has done this or if there is something obvious I'm missing.

I'm using ^5.1.1 in my original project, I tried a newer version but no dice.

fudgepop01 commented 3 years ago

I managed to get it working (AND get past that error) - As you pointed out, the error occurs within the Buffer package's copy function. This happens because under the hood, slippi-js appears to use UInt8Arrays to initialize things when it becomes browserified (...i think). what I did was override the Buffer.prototype.copy method to forcibly turn the UInt8Array target into an actual Buffer via the Buffer library's Buffer.from method.

Here's the code that did it:

  import { Buffer } from "buffer/index";

  window["Buffer"] = Buffer; // just in case
  // store the original copy method for later
  Buffer.prototype["copySub"] = Buffer.prototype.copy;
  // override the buffer's copy method to force the "target" to be a Buffer
  Buffer.prototype.copy = function copy (target, targetStart, start, end) {
    // it is EXTREMELY IMPORTANT to use target.buffer here otherwise it will not actually copy the data properly
    if (!Buffer.isBuffer(target)) target = Buffer.from(target.buffer);
    const out = Buffer.prototype["copySub"].call(Buffer.from(this), target, targetStart, start, end);
    return out;
  }

I hope this helps! :D

NikhilNarayana commented 3 years ago

We now support ArrayBuffer input as of 6.1.2, if this does not fit your needs then please reopen with some clarifications. Thanks.