Created
June 25, 2024 11:43
-
-
Save CurtisAccelerate/e56ec3944b48a41937aa1f4a6c83f08d to your computer and use it in GitHub Desktop.
ChatGPT C64 Run Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name ChatGPT Code Block Processor C64 Edition | |
// @namespace http://tampermonkey.net/ | |
// @version 3.0.0 | |
// @description Add "Run HTML" and "Run C64" buttons as hover actions on code blocks in ChatGPT responses with a draggable panel. | |
// @match https://chatgpt.com/* | |
// @grant GM_addElement | |
// @grant GM_addStyle | |
// @grant unsafeWindow | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
let panel; | |
let isDragging = false; | |
let startX; | |
let startWidth; | |
// Define variables and functions | |
let c64Iframe = null; | |
function sendToC64(text) { | |
if (!c64Iframe) { | |
console.error('C64 iframe not initialized'); | |
return false; | |
} | |
console.log('Sending to C64:', text); | |
try { | |
c64Iframe.contentWindow.postMessage({ type: 'typePetText', text: text + "\r\n" }, '*'); | |
return true; | |
} catch (error) { | |
console.error('Error sending message to C64:', error); | |
return false; | |
} | |
} | |
function checkC64IframeStatus() { | |
console.log('c64Iframe:', c64Iframe); | |
console.log('c64Iframe.contentWindow:', c64Iframe ? c64Iframe.contentWindow : 'N/A'); | |
if (c64Iframe && c64Iframe.contentWindow) { | |
console.log('C64 iframe is ready to receive messages'); | |
return true; | |
} else { | |
console.log('C64 iframe is not ready'); | |
return false; | |
} | |
} | |
function waitForIframeReady(callback) { | |
const checkInterval = 5; // Check every 100 milliseconds | |
const maxAttempts = 10; // Maximum attempts before giving up | |
let attempts = 0; | |
const interval = setInterval(() => { | |
if (checkC64IframeStatus()) { | |
clearInterval(interval); | |
callback(); | |
} else if (attempts >= maxAttempts) { | |
clearInterval(interval); | |
console.error('C64 iframe did not become ready in time'); | |
} | |
attempts++; | |
}, checkInterval); | |
} | |
// Expose variables and functions to the global scope | |
unsafeWindow.sendToC64 = sendToC64; | |
unsafeWindow.checkC64IframeStatus = checkC64IframeStatus; | |
function createSlideOutPanel(codeBlock, isC64 = false) { | |
if (panel) { | |
panel.remove(); | |
} | |
panel = document.createElement('div'); | |
panel.style.cssText = ` | |
position: fixed; | |
top: 0; | |
right: 0; | |
width: 600px; | |
height: 100%; | |
background: #f7f7f8; | |
box-shadow: -2px 0 5px rgba(0,0,0,0.3); | |
z-index: 1000; | |
display: flex; | |
flex-direction: column; | |
transform: translateX(100%); | |
transition: transform 0.3s ease-in-out; | |
`; | |
const header = document.createElement('div'); | |
header.style.cssText = ` | |
padding: 10px; | |
background: #282c34; | |
color: #fff; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
cursor: move; | |
`; | |
const closeButton = document.createElement('button'); | |
closeButton.textContent = 'Close'; | |
closeButton.style.cssText = ` | |
background: #ff5f57; | |
border: none; | |
border-radius: 5px; | |
padding: 5px 10px; | |
cursor: pointer; | |
color: white; | |
`; | |
closeButton.onclick = () => panel.style.transform = 'translateX(100%)'; | |
header.appendChild(closeButton); | |
panel.appendChild(header); | |
const contentContainer = document.createElement('div'); | |
contentContainer.style.cssText = ` | |
padding: 10px; | |
overflow-y: auto; | |
flex-grow: 1; | |
`; | |
const iframe = document.createElement('iframe'); | |
iframe.style.cssText = ` | |
width: 100%; | |
height: 100%; | |
border: none; | |
margin: 0; | |
padding: 0; | |
`; | |
contentContainer.appendChild(iframe); | |
panel.appendChild(contentContainer); | |
document.body.appendChild(panel); | |
if (isC64) { | |
iframe.src = 'http://localhost:8080'; // Adjust to your C64 emulator URL | |
c64Iframe = iframe; // Store the iframe globally | |
iframe.onload = () => { | |
console.log('C64 iframe loaded'); | |
const basicCode = codeBlock.querySelector('code').textContent.trim(); | |
console.log('BASIC code to be sent:', basicCode); | |
// Ensure the iframe is ready before sending the code | |
setTimeout(() => { | |
waitForIframeReady(() => { | |
console.log('Sending BASIC code to C64 emulator'); | |
sendToC64(basicCode); | |
}); | |
}, 1000); // Give some time for the iframe to initialize | |
}; | |
} else { | |
const cleanCodeBlock = codeBlock.cloneNode(true); | |
const runDemoButton = cleanCodeBlock.querySelector('.run-demo-hover-button'); | |
if (runDemoButton) { | |
runDemoButton.remove(); | |
} | |
const doc = iframe.contentDocument || iframe.contentWindow.document; | |
doc.open(); | |
doc.write(cleanCodeBlock.textContent); | |
doc.close(); | |
} | |
setTimeout(() => panel.style.transform = 'translateX(0)', 0); | |
header.addEventListener('mousedown', startDragging); | |
document.addEventListener('mousemove', drag); | |
document.addEventListener('mouseup', stopDragging); | |
document.addEventListener('click', function(event) { | |
if (!panel.contains(event.target) && !event.target.closest('.run-demo-hover-button')) { | |
panel.style.transform = 'translateX(100%)'; | |
} | |
}, { once: true }); | |
} | |
function startDragging(e) { | |
isDragging = true; | |
startX = e.clientX; | |
startWidth = parseInt(document.defaultView.getComputedStyle(panel).width, 10); | |
} | |
function drag(e) { | |
if (!isDragging) return; | |
const width = startWidth - (e.clientX - startX); | |
panel.style.width = `${width}px`; | |
} | |
function stopDragging() { | |
isDragging = false; | |
} | |
function addHoverButton(codeBlock) { | |
if (!codeBlock.querySelector('.run-demo-hover-button')) { | |
const htmlButton = document.createElement('button'); | |
htmlButton.textContent = 'Run HTML'; | |
htmlButton.className = 'run-demo-hover-button'; | |
htmlButton.style.cssText = ` | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
padding: 5px 10px; | |
background-color: #4CAF50; | |
color: white; | |
border: none; | |
border-radius: 3px; | |
cursor: pointer; | |
display: none; | |
z-index: 1000; | |
`; | |
htmlButton.onclick = (e) => { | |
e.stopPropagation(); | |
createSlideOutPanel(codeBlock); | |
}; | |
const c64Button = document.createElement('button'); | |
c64Button.textContent = 'Run C64'; | |
c64Button.className = 'run-demo-hover-button'; | |
c64Button.style.cssText = ` | |
position: absolute; | |
top: 10px; | |
right: 90px; | |
padding: 5px 10px; | |
background-color: #4CAF50; | |
color: white; | |
border: none; | |
border-radius: 3px; | |
cursor: pointer; | |
display: none; | |
z-index: 1000; | |
`; | |
c64Button.onclick = (e) => { | |
e.stopPropagation(); | |
createSlideOutPanel(codeBlock, true); | |
}; | |
codeBlock.style.position = 'relative'; | |
codeBlock.appendChild(htmlButton); | |
codeBlock.appendChild(c64Button); | |
codeBlock.addEventListener('mouseenter', () => { | |
htmlButton.style.display = 'block'; | |
c64Button.style.display = 'block'; | |
}); | |
codeBlock.addEventListener('mouseleave', () => { | |
htmlButton.style.display = 'none'; | |
c64Button.style.display = 'none'; | |
}); | |
} | |
} | |
function processCodeBlocks() { | |
const codeBlocks = document.querySelectorAll('.overflow-y-auto'); | |
codeBlocks.forEach(codeBlock => { | |
if (codeBlock.closest('.markdown')) { | |
addHoverButton(codeBlock); | |
} | |
}); | |
} | |
function observeChat() { | |
const observer = new MutationObserver((mutations) => { | |
mutations.forEach((mutation) => { | |
if (mutation.type === 'childList') { | |
mutation.addedNodes.forEach((node) => { | |
if (node.nodeType === Node.ELEMENT_NODE) { | |
const codeBlocks = node.querySelectorAll('.overflow-y-auto'); | |
codeBlocks.forEach(codeBlock => { | |
if (codeBlock.closest('.markdown')) { | |
addHoverButton(codeBlock); | |
} | |
}); | |
} | |
}); | |
} | |
}); | |
}); | |
const chatContainer = document.querySelector('main'); | |
if (chatContainer) { | |
observer.observe(chatContainer, { childList: true, subtree: true }); | |
} | |
} | |
function addIndicator() { | |
const indicator = document.createElement('div'); | |
indicator.textContent = 'Code Block Processor Active'; | |
indicator.style.cssText = ` | |
position: fixed; | |
top: 10px; | |
right: 10px; | |
z-index: 1000; | |
padding: 5px 10px; | |
background-color: #4CAF50; | |
color: white; | |
border-radius: 5px; | |
font-size: 12px; | |
`; | |
document.body.appendChild(indicator); | |
setTimeout(() => indicator.style.display = 'none', 3000); | |
} | |
function initializeProcessor() { | |
addIndicator(); | |
processCodeBlocks(); | |
observeChat(); | |
} | |
// Use MutationObserver to wait for the chat interface to load | |
const bodyObserver = new MutationObserver((mutations) => { | |
if (document.querySelector('main')) { | |
bodyObserver.disconnect(); | |
initializeProcessor(); | |
} | |
}); | |
bodyObserver.observe(document.body, { childList: true, subtree: true }); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Project: | |
Run c64 Basic code directly in ChatGPT. | |
Instructions | |
1. Install tampermokey script | |
2. Run CSP unblock. CSP policy is designed to protect you. Caution: https://chromewebstore.google.com/detail/csp-unblock/lkbelpgpclajeekijigjffllhigbhobd?hl=en | |
3. install and run this emulator locally https://github.com/luxocrates/viciious | |
4. Replace the webdev.js and runloop.js. Ensure the server is set correctly | |
5. Play the examples https://chatgpt.com/share/b11d2824-bed2-4b37-8e3f-bdeafbe8eac8 | |
6. Check out some of these basic programs | |
https://github.com/mad4j/c64-codeart | |
https://retrogamestart.com/answers/learn-commodore-64-basic-programming-type-text-based-games | |
Architecture: | |
1. Tamper moneky script monitors chat and adds button to code blocks. | |
2. Press the send c64 opens iframe with address of local running emulator | |
3. Host process for emulator watches for commands to send text | |
4. Updated with a sendPet to receive text | |
Recap quick/start | |
1. install tampermonkey script | |
2. get viciious c64 emulator | |
3. update the files | |
4. start the c64 emulator | |
5. use the extension script to disable security policy for the chatgpt | |
6. click button to run the c64 demo code | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
runloop: performs operations like tick, serialize and deserialize on the | |
target system as a whole, and runs the simulation in a time-throttled loop, | |
with an optional hook for breakpoints | |
*/ | |
import { | |
KEYBOARD_BUFFER_ADDR, | |
KEYBOARD_BUFFER_INDEX, | |
KEYBOARD_BUFFER_LENGTH, | |
} from "../tools/romLocations"; | |
// Node.js doesn't have `performance` loaded by default. Not that we make use | |
// of it in that version, but the runloop expects it to be there. | |
if (!globalThis.performance) { | |
globalThis.performance = { now: () => 0 }; | |
} | |
// How many frames to run for a single frames-per-second sample | |
const FRAMES_PER_WAYPOINT = 50; | |
// Bound by attach | |
let c64; | |
// configure by setDevices | |
let wires; | |
let cpu; | |
let vic; | |
let cias; | |
let sid; | |
let tape; | |
let state; | |
let masterStop = false; | |
let frameStop = false; | |
export function attach(nascentC64) { | |
c64 = nascentC64; | |
wires = c64.wires; | |
cpu = c64.cpu; | |
vic = c64.vic; | |
cias = c64.cias; | |
sid = c64.sid; | |
tape = c64.tape; | |
reset(); | |
c64.runloop = { | |
// Control | |
run, | |
stop, | |
stopAfterFrame, | |
isRunning, | |
type, | |
untilPc, | |
reset, | |
serialize, | |
deserialize, | |
typePet, | |
// Debug | |
getState, | |
}; | |
} | |
function reset() { | |
state = { | |
cycle: 0, | |
}; | |
c64.wires.reset(); | |
c64.ram .reset(); | |
c64.vic .reset(); | |
c64.sid .reset(); | |
c64.cpu .reset(); | |
c64.cias .reset(); | |
c64.tape .reset(); | |
if (c64.hooks.setTitle) { | |
c64.hooks.setTitle(""); | |
} | |
} | |
function getState() { | |
return state; | |
} | |
function stop() { | |
masterStop = true; | |
} | |
function stopAfterFrame() { | |
frameStop = true; | |
} | |
function isRunning() { | |
return !masterStop; | |
} | |
let timer; | |
function run(profile) { | |
// Apply default run profile | |
profile = { | |
tick: () => false, | |
fps: 50, | |
...profile, | |
} | |
let resolveBreakPromise; | |
const breakPromise = new Promise(resolve => { resolveBreakPromise = resolve; }); | |
if (timer !== undefined) { | |
masterStop = true; | |
clearInterval(timer); | |
} | |
masterStop = false; | |
frameStop = false; | |
const cleanUpOnBreak = () => { | |
clearInterval(timer); | |
if (c64.hooks.didStop) c64.hooks.didStop(); | |
timer = undefined; | |
resolveBreakPromise(); | |
}; | |
let timeAtWaypoint = performance.now(); | |
let framesSinceWaypoint = 0; | |
timer = setInterval( | |
() => { | |
try { | |
// We'll loop for one video frame at a time. That is, | |
// 312 rows of 63 cycles per row | |
// (Which would be different if we support NTSC in future) | |
for (let i = 0; i < (63 * 312); i++) { | |
state.cycle++; | |
cpu .tick(); | |
vic .tick(); | |
cias.tick(); | |
sid .tick(); | |
tape.tick(); | |
if (masterStop || profile.tick()) { | |
cleanUpOnBreak(); | |
break; | |
} | |
} | |
// Frames-per-second counter | |
if (++framesSinceWaypoint === FRAMES_PER_WAYPOINT) { | |
const now = performance.now(); | |
if (c64.hooks.updateFps) { | |
c64.hooks.updateFps( | |
Math.round((1000 * FRAMES_PER_WAYPOINT) / (now - timeAtWaypoint)) | |
); | |
} | |
timeAtWaypoint = now; | |
framesSinceWaypoint = 0; | |
} | |
// Frame stop | |
if (frameStop) cleanUpOnBreak(); | |
} | |
catch (e) { | |
console.error("Caught exception:", e); | |
cleanUpOnBreak(); | |
} | |
}, | |
1000 / profile.fps | |
); | |
if (c64.hooks.didStart) c64.hooks.didStart(); | |
return breakPromise; | |
} | |
async function untilPc(pc, fast = false) { | |
const regs = c64.cpu.getState(); | |
if (pc === undefined) { | |
// TODO: throw instead? | |
console.error("Missing argument: PC address"); | |
return; | |
} | |
// If the PC was currently at the address we were waiting for, | |
// advance past it. You want to be able to call this function | |
// multple times to re-run. | |
await run({ | |
tick: () => regs.pc !== pc, | |
}); | |
const profile = { | |
tick: () => regs.pc === pc, | |
}; | |
if (fast) profile.fps = Infinity; | |
return run(profile); | |
} | |
function type(str) { | |
let bufLen = c64.wires.cpuRead(KEYBOARD_BUFFER_INDEX); | |
for (let char of str) { | |
if (bufLen >= KEYBOARD_BUFFER_LENGTH) { | |
throw new Error("Overflow for Kernal keyboard buffer"); | |
} | |
c64.wires.cpuWrite(KEYBOARD_BUFFER_ADDR + bufLen, char.charCodeAt(0)); | |
c64.wires.cpuWrite(KEYBOARD_BUFFER_INDEX, ++bufLen); | |
} | |
} | |
async function typePet(str) { | |
let bufLen = c64.wires.cpuRead(KEYBOARD_BUFFER_INDEX); | |
for (let char of str) { | |
while (bufLen >= KEYBOARD_BUFFER_LENGTH) { | |
// Wait for space in the buffer | |
await new Promise(resolve => setTimeout(resolve, 10)); | |
bufLen = c64.wires.cpuRead(KEYBOARD_BUFFER_INDEX); | |
} | |
let petsciiChar = convertToPetscii(char); | |
c64.wires.cpuWrite(KEYBOARD_BUFFER_ADDR + bufLen, petsciiChar); | |
c64.wires.cpuWrite(KEYBOARD_BUFFER_INDEX, ++bufLen); | |
} | |
} | |
function convertToPetscii(char) { | |
const asciiCode = char.charCodeAt(0); | |
let petsciiCode; | |
// Convert ASCII to PETSCII | |
if (asciiCode >= 65 && asciiCode <= 90) { // A-Z | |
petsciiCode = asciiCode - 64 + 192; // Convert A-Z to PETSCII uppercase | |
} else if (asciiCode >= 97 && asciiCode <= 122) { // a-z | |
petsciiCode = asciiCode - 96 + 64; // Convert a-z to PETSCII lowercase | |
} else if (asciiCode === 13) { // Carriage return | |
petsciiCode = 13; | |
} else if (asciiCode === 10) { // Newline | |
petsciiCode = 13; // Convert newline to carriage return in PETSCII | |
} else { | |
petsciiCode = asciiCode; // Default case for other characters | |
} | |
return petsciiCode; | |
} | |
function serialize() { | |
return JSON.stringify( | |
{ | |
version: { | |
creator: "viciious", | |
major: 0, | |
minor: 1, | |
}, | |
runloop: JSON.stringify(state), | |
wires: c64.wires.serialize(), | |
ram: c64.ram .serialize(), | |
vic: c64.vic .serialize(), | |
sid: c64.sid .serialize(), | |
cpu: c64.cpu .serialize(), | |
cias: c64.cias .serialize(), | |
tape: c64.tape .serialize(), | |
} | |
); | |
} | |
function deserialize(json) { | |
const obj = JSON.parse(json); | |
state = JSON.parse(obj.runloop); | |
c64.wires.deserialize(obj.wires); | |
c64.ram .deserialize(obj.ram ); | |
c64.vic .deserialize(obj.vic ); | |
c64.sid .deserialize(obj.sid ); | |
c64.cpu .deserialize(obj.cpu ); | |
c64.cias .deserialize(obj.cias ); | |
c64.tape .deserialize(obj.tape ); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Host interfaces | |
import { attach as video } from "../host/video-canvas"; | |
import { attach as audio } from "../host/audio-OscillatorNode"; | |
import { attach as joystick } from "../host/joystick-KeyboardEvent"; | |
import { attach as keyboard } from "../host/keyboard-KeyboardEvent"; | |
// Target devices | |
import { attach as wires } from "../target/wires"; | |
import { attach as ram } from "../target/ram"; | |
import { attach as vic } from "../target/vic"; | |
import { attach as sid } from "../target/sid"; | |
import { attach as cias } from "../target/cias"; | |
import { attach as cpu } from "../target/cpu"; | |
import { attach as tape } from "../target/tape"; | |
// ROMs | |
import basic from "../target/rom/basic"; | |
import kernal from "../target/rom/skipRamTest"; | |
import character from "../target/rom/character"; | |
// Bringup | |
import { bringup } from "../target/bringup"; | |
// Everything else | |
import { attach as monitor } from "../monitor"; | |
import { attach as webFrontEnd } from "../host/webFrontEnd"; | |
import { attach as dragAndDrop } from "../host/dragAndDrop"; | |
const c64 = bringup({ | |
host: { audio, video, keyboard, joystick }, | |
target: { wires, ram, vic, sid, cpu, cias, tape, basic, kernal, character }, | |
attachments: [ | |
monitor, | |
dragAndDrop, | |
webFrontEnd, | |
], | |
}); | |
c64.runloop.run(); | |
// Make the c64 object globally accessible | |
globalThis.c64 = c64; | |
// Add event listener for messages | |
window.addEventListener('message', (event) => { | |
// Ensure the message is coming from a trusted source | |
const command = event.data; | |
// Execute the command | |
if (command.type === 'startRunloop') { | |
c64.runloop.run(); | |
} else if (command.type === 'stopRunloop') { | |
c64.runloop.stop(); | |
} else if (command.type === 'typeText') { | |
c64.runloop.type(command.text); | |
} else if (command.type === 'typePetText') { | |
c64.runloop.typePet(command.text); | |
} | |
// Add more commands as needed | |
}); | |
// To run a test program on load, uncomment the below: | |
/* | |
import { ingest } from "../host/ingest"; | |
import prg from "../tests/tod-prg.js"; | |
ingest(c64, ".prg", prg); | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment