Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mui-z committed Sep 25, 2022
0 parents commit 130b102
Show file tree
Hide file tree
Showing 9 changed files with 587 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/swift.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Swift

on: [push]

jobs:
build:

runs-on: macos-latest

steps:
- uses: swift-actions/setup-swift@v1
with:
swift-version: "5.7.0"
- name: Get swift version
run: swift --version
- uses: actions/checkout@v3
- name: Build
run: swift build | xcpretty
- name: Run tests
run: swift test | xcpretty
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.idea/
*.sn
32 changes: 32 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "EffectiveNovelCore",
platforms: [
.macOS(.v10_15),
.iOS(.v16)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "EffectiveNovelCore",
targets: ["EffectiveNovelCore"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "EffectiveNovelCore",
dependencies: []),
.testTarget(
name: "EffectiveNovelCoreTests",
dependencies: ["EffectiveNovelCore"]),
]
)
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Effective Novel Core

This is novel text parse and stream provide package.

## About
This is novel engine.

This effectively helps to display the characters of the novel.  

This lib doesn't include UI layer.
This only provides parse and output stream display event. (I'm going to will link CLI and iOS Example.)

Also, this lib is not optimized novel game.
Because this doesn't have if functioned, macro, subroutine.

## Syntax
| command | DisplayEvent | mean |
|------------------|-------------------------|---------------------------------------------------------------------|
| n | `.newline` | newline |
| tw | `.tapWait` | tap wait |
| twn | `tapWaitAndNewline` | tap wait and newline |
| cl | `.clear` | clear |
| delay speed=xxxx | `.delay(speed: Double)` | change delay character displayed speed. speed unit is milliseconds. |
| resetdelay | `.resetDelay` | reset delay speed |
| e | `.end` | stop script novel end point |

```
start text[n]
tap waiting and newline[twn]
[cl] cleared text.
very fast stream after this text[delay speed=2][n]
[resetdelay]reset delay speed.[n]
end. [e]
```

## Usage

```swift

// 1. get `NovelController` instance
let controller = NovelController()

// 2. pass to raw novel text
controller.load(rawText: rawText)

// 3. start() and listening stream
controller.start()
.sink { event in
switch event {
case .character(let char):
displayCharacter(char)
// and any command handling
}
}
.store(in: &cancellables)

// (4.) show text until wait tag
controller.showTextUntilWaitTag()

// (5.) resume tap wait
// If you want to start from any index number, you can use `controller.resume(at: 100)`
controller.resume()

// (6.) interrupt
controller.interrupt()


```



## Examples
- [ ] CLI novel reader
- [ ] iOS novel reader

## Todo
- value input
163 changes: 163 additions & 0 deletions Sources/EffectiveNovelCore/Controller/Controller.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//
// Created by osushi on 2022/09/23.
//

import Foundation
import Combine

enum NovelState {
case loadWait, prepare, running, pause
}

let defaultSpeed: Double = 250

public class NovelController {

public init() {
}

private var internalOutputStream = PassthroughSubject<DisplayEvent, Never>()

private var displayEvents: [DisplayEvent] = []

private var cancellable: Set<AnyCancellable> = []

private(set) var index: Int = 0

private(set) var speed: Double = defaultSpeed

private(set) var state = NovelState.loadWait

public func load(raw: String) {
let parser = ScriptParser()
state = .prepare
index = 0

displayEvents = parser.parse(rawAllString: raw)
}

public func start() -> AnyPublisher<DisplayEvent, Never> {
switch state {
case .prepare:
state = .running
startLoop()
default:
print("now state is not prepare. now state: \(state)")
}

return internalOutputStream.eraseToAnyPublisher()
}

public func interrupt() {
switch state {
case .running, .pause:
reset()
default:
print("now state is not running or pause. now state: \(state)")
}
}

public func resume() {
switch state {
case .pause:
state = .running
default:
print("now state is not pause. now state: \(state)")
}
}

public func resume(at resumeIndex: Int) {
switch state {
case .pause:
index = resumeIndex
state = .running
default:
print("now state is not pause. now state: \(state)")
}
}

public func reset() {
state = .loadWait
displayEvents = []
index = 0
}

public func showTextUntilWaitTag() {
let offset: Int = index

let checkListRange = displayEvents[offset..<displayEvents.count]
let endIndex = checkListRange
.firstIndex(where: { $0 == .tapWaitAndNewline || $0 == .tapWait || $0 == .newline })
.map { $0 + index } ?? index

let events = displayEvents[(index + 1)...(endIndex - 1)]

index += events.count
events.forEach { internalOutputStream.send($0) }
}

private func startLoop() {
Task {
// NOTE: wait for preparing subscribe stream client.
try! await Task.sleep(nanoseconds: 100000)

while (true) {
switch state {
case .running:
if displayEvents.isEmpty {
reset()
break
}

let event = displayEvents[index]

internalOutputStream.send(event)

// handle on interrupted
if state == .loadWait {
reset()
break
}

// finish
if displayEvents[index] == .end {
state = .prepare
break
}

index += 1
await handleEvent(event: event)
case .prepare:
break
case .loadWait:
break
default:
// clock
try! await Task.sleep(nanoseconds: 10)
continue
}

// interrupt
if state == .prepare { break }

// clock
try! await Task.sleep(nanoseconds: 10)
}
}
}

private func handleEvent(event: DisplayEvent) async {
switch event {
case .character(_):
try! await Task.sleep(nanoseconds: UInt64(speed * Double(pow(10.0, 6))))
case .delay(let duration):
speed = duration
case .resetDelay:
speed = defaultSpeed
case .tapWait, .tapWaitAndNewline:
state = .pause
default:
print("default")
}
}
}
40 changes: 40 additions & 0 deletions Sources/EffectiveNovelCore/Parser/ScriptParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Created by osushi on 2022/09/23.
//

import Foundation

protocol Parser {
func parse(rawAllString: String) -> [DisplayEvent]
}

struct ScriptParser: Parser {
func parse(rawAllString raw: String) -> [DisplayEvent] {
var rawAllString = raw
rawAllString.removeAll(where: { $0 == "\n" })

return rawAllString.components(separatedBy: "[")
.filter { !$0.isEmpty }
.map { (raw: $0, isCommandInclude: $0.contains("]")) }
.map { $0.isCommandInclude ? splitCommandIncludingText(raw: $0.raw) : stringToCharacter(string: $0.raw) }
.flatMap { $0 }
}

// cm] or cm]text
private func splitCommandIncludingText(raw: String) -> [DisplayEvent] {
var result: [DisplayEvent] = []
let commandAndText = raw.components(separatedBy: "]")

result.append(DisplayEvent.parseCommand(rawCommand: commandAndText.first!))

if let text = commandAndText.last, !text.isEmpty {
result += stringToCharacter(string: text)
}

return result
}

private func stringToCharacter(string: String) -> [DisplayEvent] {
string.map { c in DisplayEvent.character(char: c) }
}
}
38 changes: 38 additions & 0 deletions Sources/EffectiveNovelCore/Syntax/NovelSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Created by osushi on 2022/09/23.
//

public enum DisplayEvent: Equatable {
case character(char: Character)
case newline
case tapWait
case tapWaitAndNewline
case clear

case resetDelay
case delay(speed: Double)

case end

static func parseCommand(rawCommand: String) -> DisplayEvent {
switch rawCommand {
case "n":
return .newline
case "tw":
return .tapWait
case "twn":
return .tapWaitAndNewline
case "cl":
return .clear
case "resetdelay":
return .resetDelay
case (let command) where command.contains("delay"):
let speed = Double(command.split(separator: "=").last!)!
return .delay(speed: speed)
case "e":
return .end
default:
fatalError(file: #file)
}
}
}
Loading

0 comments on commit 130b102

Please sign in to comment.